diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..79fe802 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +insert_final_newline = true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..3466553 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +dist/ +test/ \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..4dee9b0 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,12 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ] +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..f95bb33 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/gts/" +} diff --git a/.gitignore b/.gitignore index 405d6f3..1a65c52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ dest/ -*.js \ No newline at end of file +*.js +dist +node_modules \ No newline at end of file diff --git a/LICENSE b/LICENSE index 7a4a3ea..dfadee9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,191 +1,191 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. Copyright [yyyy] [name of copyright owner] @@ -193,7 +193,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/README.md b/README.md index ea0fd78..dcfe196 100644 --- a/README.md +++ b/README.md @@ -58,4 +58,3 @@ import {CspParser} from "csp_evaluator/dist/parser.js"; const parsed = new CspParser("script-src https://google.com").csp; console.log(new CspEvaluator(parsed).evaluate()); ``` - diff --git a/checks/parser_checks.ts b/checks/parser_checks.ts deleted file mode 100644 index 4d64e8b..0000000 --- a/checks/parser_checks.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * @fileoverview Collection of CSP parser checks which can be used to find - * common syntax mistakes like missing semicolons, invalid directives or - * invalid keywords. - * @author lwe@google.com (Lukas Weichselbaum) - * - * @license - * Copyright 2016 Google Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as csp from '../csp'; -import {Csp, Keyword} from '../csp'; - -import {Finding, Severity, Type} from '../finding'; - - -/** - * Checks if the csp contains invalid directives. - * - * Example policy where this check would trigger: - * foobar-src foo.bar - * - * @param parsedCsp A parsed csp. - */ -export function checkUnknownDirective(parsedCsp: Csp): Finding[] { - const findings: Finding[] = []; - - for (const directive of Object.keys(parsedCsp.directives)) { - if (csp.isDirective(directive)) { - // Directive is known. - continue; - } - - if (directive.endsWith(':')) { - findings.push(new Finding( - Type.UNKNOWN_DIRECTIVE, 'CSP directives don\'t end with a colon.', - Severity.SYNTAX, directive)); - } else { - findings.push(new Finding( - Type.UNKNOWN_DIRECTIVE, - 'Directive "' + directive + '" is not a known CSP directive.', - Severity.SYNTAX, directive)); - } - } - - return findings; -} - - -/** - * Checks if semicolons are missing in the csp. - * - * Example policy where this check would trigger (missing semicolon before - * start of object-src): - * script-src foo.bar object-src 'none' - * - * @param parsedCsp A parsed csp. - */ -export function checkMissingSemicolon(parsedCsp: Csp): Finding[] { - const findings: Finding[] = []; - - for (const [directive, directiveValues] of Object.entries( - parsedCsp.directives)) { - if (directiveValues === undefined) { - continue; - } - for (const value of directiveValues) { - // If we find a known directive inside a directive value, it is very - // likely that a semicolon was forgoten. - if (csp.isDirective(value)) { - findings.push(new Finding( - Type.MISSING_SEMICOLON, - 'Did you forget the semicolon? ' + - '"' + value + '" seems to be a directive, not a value.', - Severity.SYNTAX, directive, value)); - } - } - } - - return findings; -} - - -/** - * Checks if csp contains invalid keywords. - * - * Example policy where this check would trigger: - * script-src 'notAkeyword' - * - * @param parsedCsp A parsed csp. - */ -export function checkInvalidKeyword(parsedCsp: Csp): Finding[] { - const findings: Finding[] = []; - const keywordsNoTicks = - Object.values(Keyword).map((k) => k.replace(/'/g, '')); - - for (const [directive, directiveValues] of Object.entries( - parsedCsp.directives)) { - if (directiveValues === undefined) { - continue; - } - for (const value of directiveValues) { - // Check if single ticks have been forgotten. - if (keywordsNoTicks.some((k) => k === value) || - value.startsWith('nonce-') || - value.match(/^(sha256|sha384|sha512)-/)) { - findings.push(new Finding( - Type.INVALID_KEYWORD, - 'Did you forget to surround "' + value + '" with single-ticks?', - Severity.SYNTAX, directive, value)); - continue; - } - - // Continue, if the value doesn't start with single tick. - // All CSP keywords start with a single tick. - if (!value.startsWith('\'')) { - continue; - } - - if (directive === csp.Directive.REQUIRE_TRUSTED_TYPES_FOR) { - // Continue, if it's an allowed Trusted Types sink. - if (value === csp.TrustedTypesSink.SCRIPT) { - continue; - } - } else if (directive === csp.Directive.TRUSTED_TYPES) { - // Continue, if it's an allowed Trusted Types keyword. - if (value === '\'allow-duplicates\'' || value === '\'none\'') { - continue; - } - } else { - // Continue, if it's a valid keyword. - if (csp.isKeyword(value) || csp.isHash(value) || csp.isNonce(value)) { - continue; - } - } - - findings.push(new Finding( - Type.INVALID_KEYWORD, value + ' seems to be an invalid CSP keyword.', - Severity.SYNTAX, directive, value)); - } - } - - return findings; -} - diff --git a/checks/security_checks.ts b/checks/security_checks.ts deleted file mode 100644 index 1fd9dc4..0000000 --- a/checks/security_checks.ts +++ /dev/null @@ -1,578 +0,0 @@ -/** - * @fileoverview Collection of CSP evaluation checks. - * @author lwe@google.com (Lukas Weichselbaum) - * - * @license - * Copyright 2016 Google Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as angular from '../allowlist_bypasses/angular'; -import * as flash from '../allowlist_bypasses/flash'; -import * as jsonp from '../allowlist_bypasses/jsonp'; -import * as csp from '../csp'; -import {Csp, Directive, Keyword} from '../csp'; -import {Finding, Severity, Type} from '../finding'; -import * as utils from '../utils'; - - -/** - * A list of CSP directives that can allow XSS vulnerabilities if they fail - * validation. - */ -export const DIRECTIVES_CAUSING_XSS: Directive[] = - [Directive.SCRIPT_SRC, Directive.OBJECT_SRC, Directive.BASE_URI]; - -/** - * A list of URL schemes that can allow XSS vulnerabilities when requests to - * them are made. - */ -export const URL_SCHEMES_CAUSING_XSS: string[] = ['data:', 'http:', 'https:']; - - -/** - * Checks if passed csp allows inline scripts. - * Findings of this check are critical and FP free. - * unsafe-inline is ignored in the presence of a nonce or a hash. This check - * does not account for this and therefore the effectiveCsp needs to be passed. - * - * Example policy where this check would trigger: - * script-src 'unsafe-inline' - * - * @param effectiveCsp A parsed csp that only contains values which - * are active in a certain version of CSP (e.g. no unsafe-inline if a nonce - * is present). - */ -export function checkScriptUnsafeInline(effectiveCsp: Csp): Finding[] { - const directiveName = - effectiveCsp.getEffectiveDirective(Directive.SCRIPT_SRC); - const values: string[] = effectiveCsp.directives[directiveName] || []; - - // Check if unsafe-inline is present. - if (values.includes(Keyword.UNSAFE_INLINE)) { - return [new Finding( - Type.SCRIPT_UNSAFE_INLINE, - `'unsafe-inline' allows the execution of unsafe in-page scripts ` + - 'and event handlers.', - Severity.HIGH, directiveName, Keyword.UNSAFE_INLINE)]; - } - - return []; -} - - -/** - * Checks if passed csp allows eval in scripts. - * Findings of this check have a medium severity and are FP free. - * - * Example policy where this check would trigger: - * script-src 'unsafe-eval' - * - * @param parsedCsp Parsed CSP. - */ -export function checkScriptUnsafeEval(parsedCsp: Csp): Finding[] { - const directiveName = parsedCsp.getEffectiveDirective(Directive.SCRIPT_SRC); - const values: string[] = parsedCsp.directives[directiveName] || []; - - // Check if unsafe-eval is present. - if (values.includes(Keyword.UNSAFE_EVAL)) { - return [new Finding( - Type.SCRIPT_UNSAFE_EVAL, - `'unsafe-eval' allows the execution of code injected into DOM APIs ` + - 'such as eval().', - Severity.MEDIUM_MAYBE, directiveName, Keyword.UNSAFE_EVAL)]; - } - - return []; -} - - -/** - * Checks if plain URL schemes (e.g. http:) are allowed in sensitive directives. - * Findings of this check have a high severity and are FP free. - * - * Example policy where this check would trigger: - * script-src https: http: data: - * - * @param parsedCsp Parsed CSP. - */ -export function checkPlainUrlSchemes(parsedCsp: Csp): Finding[] { - const violations: Finding[] = []; - const directivesToCheck = - parsedCsp.getEffectiveDirectives(DIRECTIVES_CAUSING_XSS); - - for (const directive of directivesToCheck) { - const values = parsedCsp.directives[directive] || []; - for (const value of values) { - if (URL_SCHEMES_CAUSING_XSS.includes(value)) { - violations.push(new Finding( - Type.PLAIN_URL_SCHEMES, - value + ' URI in ' + directive + ' allows the execution of ' + - 'unsafe scripts.', - Severity.HIGH, directive, value)); - } - } - } - - return violations; -} - - -/** - * Checks if csp contains wildcards in sensitive directives. - * Findings of this check have a high severity and are FP free. - * - * Example policy where this check would trigger: - * script-src * - * - * @param parsedCsp Parsed CSP. - */ -export function checkWildcards(parsedCsp: Csp): Finding[] { - const violations: Finding[] = []; - const directivesToCheck = - parsedCsp.getEffectiveDirectives(DIRECTIVES_CAUSING_XSS); - - for (const directive of directivesToCheck) { - const values = parsedCsp.directives[directive] || []; - for (const value of values) { - const url = utils.getSchemeFreeUrl(value); - if (url === '*') { - violations.push(new Finding( - Type.PLAIN_WILDCARD, directive + ` should not allow '*' as source`, - Severity.HIGH, directive, value)); - continue; - } - } - } - - return violations; -} - -/** - * Checks if object-src is restricted to none either directly or via a - * default-src. - */ -export function checkMissingObjectSrcDirective(parsedCsp: Csp): Finding[] { - let objectRestrictions: string[]|undefined = []; - if (Directive.OBJECT_SRC in parsedCsp.directives) { - objectRestrictions = parsedCsp.directives[Directive.OBJECT_SRC]; - } else if (Directive.DEFAULT_SRC in parsedCsp.directives) { - objectRestrictions = parsedCsp.directives[Directive.DEFAULT_SRC]; - } - if (objectRestrictions !== undefined && objectRestrictions.length >= 1) { - return []; - } - return [new Finding( - Type.MISSING_DIRECTIVES, - `Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`, - Severity.HIGH, Directive.OBJECT_SRC)]; -} - -/** - * Checks if script-src is restricted either directly or via a default-src. - */ -export function checkMissingScriptSrcDirective(parsedCsp: Csp): Finding[] { - if (Directive.SCRIPT_SRC in parsedCsp.directives || - Directive.DEFAULT_SRC in parsedCsp.directives) { - return []; - } - return [new Finding( - Type.MISSING_DIRECTIVES, 'script-src directive is missing.', - Severity.HIGH, Directive.SCRIPT_SRC)]; -} - -/** - * Checks if the base-uri needs to be restricted and if so, whether it has been - * restricted. - */ -export function checkMissingBaseUriDirective(parsedCsp: Csp): Finding[] { - return checkMultipleMissingBaseUriDirective([parsedCsp]); -} - -/** - * Checks if the base-uri needs to be restricted and if so, whether it has been - * restricted. - */ -export function checkMultipleMissingBaseUriDirective(parsedCsps: Csp[]): - Finding[] { - // base-uri can be used to bypass nonce based CSPs and hash based CSPs that - // use strict dynamic - const needsBaseUri = (csp: Csp) => - (csp.policyHasScriptNonces() || - (csp.policyHasScriptHashes() && csp.policyHasStrictDynamic())); - const hasBaseUri = (csp: Csp) => Directive.BASE_URI in csp.directives; - - if (parsedCsps.some(needsBaseUri) && !parsedCsps.some(hasBaseUri)) { - const description = 'Missing base-uri allows the injection of base tags. ' + - 'They can be used to set the base URL for all relative (script) ' + - 'URLs to an attacker controlled domain. ' + - `Can you set it to 'none' or 'self'?`; - return [new Finding( - Type.MISSING_DIRECTIVES, description, Severity.HIGH, - Directive.BASE_URI)]; - } - return []; -} - - -/** - * Checks if all necessary directives for preventing XSS are set. - * Findings of this check have a high severity and are FP free. - * - * Example policy where this check would trigger: - * script-src 'none' - * - * @param parsedCsp Parsed CSP. - */ -export function checkMissingDirectives(parsedCsp: Csp): Finding[] { - return [ - ...checkMissingObjectSrcDirective(parsedCsp), - ...checkMissingScriptSrcDirective(parsedCsp), - ...checkMissingBaseUriDirective(parsedCsp), - ]; -} - - -/** - * Checks if allowlisted origins are bypassable by JSONP/Angular endpoints. - * High severity findings of this check are FP free. - * - * Example policy where this check would trigger: - * default-src 'none'; script-src www.google.com - * - * @param parsedCsp Parsed CSP. - */ -export function checkScriptAllowlistBypass(parsedCsp: Csp): Finding[] { - const violations: Finding[] = []; - const effectiveScriptSrcDirective = - parsedCsp.getEffectiveDirective(Directive.SCRIPT_SRC); - const scriptSrcValues = - parsedCsp.directives[effectiveScriptSrcDirective] || []; - if (scriptSrcValues.includes(Keyword.NONE)) { - return violations; - } - - for (const value of scriptSrcValues) { - if (value === Keyword.SELF) { - violations.push(new Finding( - Type.SCRIPT_ALLOWLIST_BYPASS, - `'self' can be problematic if you host JSONP, AngularJS or user ` + - 'uploaded files.', - Severity.MEDIUM_MAYBE, effectiveScriptSrcDirective, value)); - continue; - } - - // Ignore keywords, nonces and hashes (they start with a single quote). - if (value.startsWith('\'')) { - continue; - } - - // Ignore standalone schemes and things that don't look like URLs (no dot). - if (csp.isUrlScheme(value) || value.indexOf('.') === -1) { - continue; - } - - const url = '//' + utils.getSchemeFreeUrl(value); - - const angularBypass = utils.matchWildcardUrls(url, angular.URLS); - - let jsonpBypass = utils.matchWildcardUrls(url, jsonp.URLS); - - // Some JSONP bypasses only work in presence of unsafe-eval. - if (jsonpBypass) { - const evalRequired = jsonp.NEEDS_EVAL.includes(jsonpBypass.hostname); - const evalPresent = scriptSrcValues.includes(Keyword.UNSAFE_EVAL); - if (evalRequired && !evalPresent) { - jsonpBypass = null; - } - } - - if (jsonpBypass || angularBypass) { - let bypassDomain = ''; - let bypassTxt = ''; - if (jsonpBypass) { - bypassDomain = jsonpBypass.hostname; - bypassTxt = ' JSONP endpoints'; - } - if (angularBypass) { - bypassDomain = angularBypass.hostname; - bypassTxt += (bypassTxt.trim() === '') ? '' : ' and'; - bypassTxt += ' Angular libraries'; - } - - violations.push(new Finding( - Type.SCRIPT_ALLOWLIST_BYPASS, - bypassDomain + ' is known to host' + bypassTxt + - ' which allow to bypass this CSP.', - Severity.HIGH, effectiveScriptSrcDirective, value)); - } else { - violations.push(new Finding( - Type.SCRIPT_ALLOWLIST_BYPASS, - `No bypass found; make sure that this URL doesn't serve JSONP ` + - 'replies or Angular libraries.', - Severity.MEDIUM_MAYBE, effectiveScriptSrcDirective, value)); - } - } - - return violations; -} - - -/** - * Checks if allowlisted object-src origins are bypassable. - * Findings of this check have a high severity and are FP free. - * - * Example policy where this check would trigger: - * default-src 'none'; object-src ajax.googleapis.com - * - * @param parsedCsp Parsed CSP. - */ -export function checkFlashObjectAllowlistBypass(parsedCsp: Csp): Finding[] { - const violations = []; - const effectiveObjectSrcDirective = - parsedCsp.getEffectiveDirective(Directive.OBJECT_SRC); - const objectSrcValues = - parsedCsp.directives[effectiveObjectSrcDirective] || []; - - // If flash is not allowed in plugin-types, continue. - const pluginTypes = parsedCsp.directives[Directive.PLUGIN_TYPES]; - if (pluginTypes && !pluginTypes.includes('application/x-shockwave-flash')) { - return []; - } - - for (const value of objectSrcValues) { - // Nothing to do here if 'none'. - if (value === Keyword.NONE) { - return []; - } - - const url = '//' + utils.getSchemeFreeUrl(value); - const flashBypass = utils.matchWildcardUrls(url, flash.URLS); - - if (flashBypass) { - violations.push(new Finding( - Type.OBJECT_ALLOWLIST_BYPASS, - flashBypass.hostname + - ' is known to host Flash files which allow to bypass this CSP.', - Severity.HIGH, effectiveObjectSrcDirective, value)); - } else if (effectiveObjectSrcDirective === Directive.OBJECT_SRC) { - violations.push(new Finding( - Type.OBJECT_ALLOWLIST_BYPASS, - `Can you restrict object-src to 'none' only?`, Severity.MEDIUM_MAYBE, - effectiveObjectSrcDirective, value)); - } - } - - return violations; -} - -/** - * Returns whether the given string "looks" like an IP address. This function - * only uses basic heuristics and does not accept all valid IPs nor reject all - * invalid IPs. - */ -export function looksLikeIpAddress(maybeIp: string): boolean { - if (maybeIp.startsWith('[') && maybeIp.endsWith(']')) { - // Looks like an IPv6 address and not a hostname (though it may be some - // nonsense like `[foo]`) - return true; - } - if (/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(maybeIp)) { - // Looks like an IPv4 address (though it may be something like - // `500.600.700.800` - return true; - } - // Won't match IP addresses encoded in other manners (eg octal or - // decimal) - return false; -} - -/** - * Checks if csp contains IP addresses. - * Findings of this check are informal only and are FP free. - * - * Example policy where this check would trigger: - * script-src 127.0.0.1 - * - * @param parsedCsp Parsed CSP. - */ -export function checkIpSource(parsedCsp: Csp): Finding[] { - const violations: Finding[] = []; - - // Function for checking if directive values contain IP addresses. - const checkIp = (directive: string, directiveValues: string[]) => { - for (const value of directiveValues) { - const host = utils.getHostname(value); - if (looksLikeIpAddress(host)) { - // Check if localhost. - // See 4.8 in https://www.w3.org/TR/CSP2/#match-source-expression - if (host === '127.0.0.1') { - violations.push(new Finding( - Type.IP_SOURCE, - directive + ' directive allows localhost as source. ' + - 'Please make sure to remove this in production environments.', - Severity.INFO, directive, value)); - } else { - violations.push(new Finding( - Type.IP_SOURCE, - directive + ' directive has an IP-Address as source: ' + host + - ' (will be ignored by browsers!). ', - Severity.INFO, directive, value)); - } - } - } - }; - - // Apply check to values of all directives. - utils.applyCheckFunktionToDirectives(parsedCsp, checkIp); - return violations; -} - - -/** - * Checks if csp contains directives that are deprecated in CSP3. - * Findings of this check are informal only and are FP free. - * - * Example policy where this check would trigger: - * report-uri foo.bar/csp - * - * @param parsedCsp Parsed CSP. - */ -export function checkDeprecatedDirective(parsedCsp: Csp): Finding[] { - const violations = []; - - // More details: https://www.chromestatus.com/feature/5769374145183744 - if (Directive.REFLECTED_XSS in parsedCsp.directives) { - violations.push(new Finding( - Type.DEPRECATED_DIRECTIVE, - 'reflected-xss is deprecated since CSP2. ' + - 'Please, use the X-XSS-Protection header instead.', - Severity.INFO, Directive.REFLECTED_XSS)); - } - - // More details: https://www.chromestatus.com/feature/5680800376815616 - if (Directive.REFERRER in parsedCsp.directives) { - violations.push(new Finding( - Type.DEPRECATED_DIRECTIVE, - 'referrer is deprecated since CSP2. ' + - 'Please, use the Referrer-Policy header instead.', - Severity.INFO, Directive.REFERRER)); - } - - // More details: https://github.com/w3c/webappsec-csp/pull/327 - if (Directive.DISOWN_OPENER in parsedCsp.directives) { - violations.push(new Finding( - Type.DEPRECATED_DIRECTIVE, - 'disown-opener is deprecated since CSP3. ' + - 'Please, use the Cross Origin Opener Policy header instead.', - Severity.INFO, Directive.DISOWN_OPENER)); - } - return violations; -} - - -/** - * Checks if csp nonce is at least 8 characters long. - * Findings of this check are of medium severity and are FP free. - * - * Example policy where this check would trigger: - * script-src 'nonce-short' - * - * @param parsedCsp Parsed CSP. - */ -export function checkNonceLength(parsedCsp: Csp): Finding[] { - const noncePattern = new RegExp('^\'nonce-(.+)\'$'); - const violations: Finding[] = []; - - utils.applyCheckFunktionToDirectives( - parsedCsp, (directive, directiveValues) => { - for (const value of directiveValues) { - const match = value.match(noncePattern); - if (!match) { - continue; - } - // Not a nonce. - - const nonceValue = match[1]; - if (nonceValue.length < 8) { - violations.push(new Finding( - Type.NONCE_LENGTH, - 'Nonces should be at least 8 characters long.', Severity.MEDIUM, - directive, value)); - } - - if (!csp.isNonce(value, true)) { - violations.push(new Finding( - Type.NONCE_CHARSET, - 'Nonces should only use the base64 charset.', Severity.INFO, - directive, value)); - } - } - }); - - return violations; -} - - -/** - * Checks if CSP allows sourcing from http:// - * Findings of this check are of medium severity and are FP free. - * - * Example policy where this check would trigger: - * report-uri http://foo.bar/csp - * - * @param parsedCsp Parsed CSP. - */ -export function checkSrcHttp(parsedCsp: Csp): Finding[] { - const violations: Finding[] = []; - - utils.applyCheckFunktionToDirectives( - parsedCsp, (directive, directiveValues) => { - for (const value of directiveValues) { - const description = directive === Directive.REPORT_URI ? - 'Use HTTPS to send violation reports securely.' : - 'Allow only resources downloaded over HTTPS.'; - if (value.startsWith('http://')) { - violations.push(new Finding( - Type.SRC_HTTP, description, Severity.MEDIUM, directive, value)); - } - } - }); - - return violations; -} - -/** - * Checks if the policy has configured reporting in a robust manner. - */ -export function checkHasConfiguredReporting(parsedCsp: Csp): Finding[] { - const reportUriValues: string[] = - parsedCsp.directives[Directive.REPORT_URI] || []; - if (reportUriValues.length > 0) { - return []; - } - - const reportToValues: string[] = - parsedCsp.directives[Directive.REPORT_TO] || []; - if (reportToValues.length > 0) { - return [new Finding( - Type.REPORT_TO_ONLY, - `This CSP policy only provides a reporting destination via the 'report-to' directive. This directive is only supported in Chromium-based browsers so it is recommended to also use a 'report-uri' directive.`, - Severity.INFO, Directive.REPORT_TO)]; - } - - return [new Finding( - Type.REPORTING_DESTINATION_MISSING, - 'This CSP policy does not configure a reporting destination. This makes it difficult to maintain the CSP policy over time and monitor for any breakages.', - Severity.INFO, Directive.REPORT_URI)]; -} diff --git a/checks/strictcsp_checks.ts b/checks/strictcsp_checks.ts deleted file mode 100644 index 15d5c37..0000000 --- a/checks/strictcsp_checks.ts +++ /dev/null @@ -1,179 +0,0 @@ -/** - * @fileoverview Collection of "strict" CSP and backward compatibility checks. - * A "strict" CSP is based on nonces or hashes and drops the allowlist. - * These checks ensure that 'strict-dynamic' and a CSP nonce/hash are present. - * Due to 'strict-dynamic' any allowlist will get dropped in CSP3. - * The backward compatibility checks ensure that the strict nonce/hash based CSP - * will be a no-op in older browsers by checking for presence of 'unsafe-inline' - * (will be dropped in newer browsers if a nonce or hash is present) and for - * prsensence of http: and https: url schemes (will be droped in the presence of - * 'strict-dynamic' in newer browsers). - * - * @author lwe@google.com (Lukas Weichselbaum) - * - * @license - * Copyright 2016 Google Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as csp from '../csp'; -import {Csp, Keyword} from '../csp'; - -import {Finding, Severity, Type} from '../finding'; - - -/** - * Checks if 'strict-dynamic' is present. - * - * Example policy where this check would trigger: - * script-src foo.bar - * - * @param parsedCsp A parsed csp. - */ -export function checkStrictDynamic(parsedCsp: Csp): Finding[] { - const directiveName = - parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC); - const values: string[] = parsedCsp.directives[directiveName] || []; - - const schemeOrHostPresent = values.some((v) => !v.startsWith('\'')); - - // Check if strict-dynamic is present in case a host/scheme allowlist is used. - if (schemeOrHostPresent && !values.includes(Keyword.STRICT_DYNAMIC)) { - return [new Finding( - Type.STRICT_DYNAMIC, - 'Host allowlists can frequently be bypassed. Consider using ' + - '\'strict-dynamic\' in combination with CSP nonces or hashes.', - Severity.STRICT_CSP, directiveName)]; - } - - return []; -} - - -/** - * Checks if 'strict-dynamic' is only used together with a nonce or a hash. - * - * Example policy where this check would trigger: - * script-src 'strict-dynamic' - * - * @param parsedCsp A parsed csp. - */ -export function checkStrictDynamicNotStandalone(parsedCsp: Csp): Finding[] { - const directiveName = - parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC); - const values: string[] = parsedCsp.directives[directiveName] || []; - - if (values.includes(Keyword.STRICT_DYNAMIC) && - (!parsedCsp.policyHasScriptNonces() && - !parsedCsp.policyHasScriptHashes())) { - return [new Finding( - Type.STRICT_DYNAMIC_NOT_STANDALONE, - '\'strict-dynamic\' without a CSP nonce/hash will block all scripts.', - Severity.INFO, directiveName)]; - } - - return []; -} - - -/** - * Checks if the policy has 'unsafe-inline' when a nonce or hash are present. - * This will ensure backward compatibility to browser that don't support - * CSP nonces or hasehs. - * - * Example policy where this check would trigger: - * script-src 'nonce-test' - * - * @param parsedCsp A parsed csp. - */ -export function checkUnsafeInlineFallback(parsedCsp: Csp): Finding[] { - if (!parsedCsp.policyHasScriptNonces() && - !parsedCsp.policyHasScriptHashes()) { - return []; - } - - const directiveName = - parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC); - const values: string[] = parsedCsp.directives[directiveName] || []; - - if (!values.includes(Keyword.UNSAFE_INLINE)) { - return [new Finding( - Type.UNSAFE_INLINE_FALLBACK, - 'Consider adding \'unsafe-inline\' (ignored by browsers supporting ' + - 'nonces/hashes) to be backward compatible with older browsers.', - Severity.STRICT_CSP, directiveName)]; - } - - return []; -} - - -/** - * Checks if the policy has an allowlist fallback (* or http: and https:) when - * 'strict-dynamic' is present. - * This will ensure backward compatibility to browser that don't support - * 'strict-dynamic'. - * - * Example policy where this check would trigger: - * script-src 'nonce-test' 'strict-dynamic' - * - * @param parsedCsp A parsed csp. - */ -export function checkAllowlistFallback(parsedCsp: Csp): Finding[] { - const directiveName = - parsedCsp.getEffectiveDirective(csp.Directive.SCRIPT_SRC); - const values: string[] = parsedCsp.directives[directiveName] || []; - - if (!values.includes(Keyword.STRICT_DYNAMIC)) { - return []; - } - - // Check if there's already an allowlist (url scheme or url) - if (!values.some( - (v) => ['http:', 'https:', '*'].includes(v) || v.includes('.'))) { - return [new Finding( - Type.ALLOWLIST_FALLBACK, - 'Consider adding https: and http: url schemes (ignored by browsers ' + - 'supporting \'strict-dynamic\') to be backward compatible with older ' + - 'browsers.', - Severity.STRICT_CSP, directiveName)]; - } - - return []; -} - - -/** - * Checks if the policy requires Trusted Types for scripts. - * - * I.e. the policy should have the following dirctive: - * require-trusted-types-for 'script' - * - * @param parsedCsp A parsed csp. - */ -export function checkRequiresTrustedTypesForScripts(parsedCsp: Csp): Finding[] { - const directiveName = - parsedCsp.getEffectiveDirective(csp.Directive.REQUIRE_TRUSTED_TYPES_FOR); - const values: string[] = parsedCsp.directives[directiveName] || []; - - if (!values.includes(csp.TrustedTypesSink.SCRIPT)) { - return [new Finding( - Type.REQUIRE_TRUSTED_TYPES_FOR_SCRIPTS, - 'Consider requiring Trusted Types for scripts to lock down DOM XSS ' + - 'injection sinks. You can do this by adding ' + - '"require-trusted-types-for \'script\'" to your policy.', - Severity.INFO, csp.Directive.REQUIRE_TRUSTED_TYPES_FOR)]; - } - - return []; -} diff --git a/jasmine.json b/jasmine.json index 35e0adf..4539f7b 100644 --- a/jasmine.json +++ b/jasmine.json @@ -1,7 +1,7 @@ { "spec_dir": "dist", "spec_files": [ - "**/*_test.js" + "test/**/*_test.js" ], "stopSpecOnExpectationFailure": false, "random": true diff --git a/package-lock.json b/package-lock.json index c00626c..f4e9a5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,124 +1,3804 @@ { "name": "csp_evaluator", - "version": "1.0.4", - "lockfileVersion": 1, + "version": "2.0.0", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@types/jasmine": { - "version": "3.6.7", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.7.tgz", - "integrity": "sha512-8dtfiykrpe4Ysn6ONj0tOjmpDIh1vWxPk80eutSeWmyaJvAZXZ84219fS4gLrvz05eidhp7BP17WVQBaXHSyXQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "jasmine": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.7.0.tgz", - "integrity": "sha512-wlzGQ+cIFzMEsI+wDqmOwvnjTvolLFwlcpYLCqSPPH0prOQaW3P+IzMhHYn934l1imNvw07oCyX+vGUv3wmtSQ==", - "dev": true, - "requires": { - "glob": "^7.1.6", - "jasmine-core": "~3.7.0" - } - }, - "jasmine-core": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.7.1.tgz", - "integrity": "sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "packages": { + "": { + "name": "csp_evaluator", + "version": "2.0.0", + "license": "Apache-2.0", + "devDependencies": { + "@types/jasmine": "^5.1.4", + "@types/node": "20.8.2", + "@typescript-eslint/eslint-plugin": "^6.17.0", + "@typescript-eslint/parser": "^6.17.0", + "eslint": "^8.56.0", + "gts": "^5.2.0", + "jasmine": "^5.1.0", + "typescript": "~5.2.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz", + "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@types/jasmine": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.4.tgz", + "integrity": "sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", + "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", + "dev": true + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.18.0.tgz", + "integrity": "sha512-3lqEvQUdCozi6d1mddWqd+kf8KxmGq2Plzx36BlkjuQe3rSTm/O98cLf0A4uDO+a5N1KD2SeEEl6fW97YHY+6w==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.18.0", + "@typescript-eslint/type-utils": "6.18.0", + "@typescript-eslint/utils": "6.18.0", + "@typescript-eslint/visitor-keys": "6.18.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.18.0.tgz", + "integrity": "sha512-v6uR68SFvqhNQT41frCMCQpsP+5vySy6IdgjlzUWoo7ALCnpaWYcz/Ij2k4L8cEsL0wkvOviCMpjmtRtHNOKzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.18.0", + "@typescript-eslint/types": "6.18.0", + "@typescript-eslint/typescript-estree": "6.18.0", + "@typescript-eslint/visitor-keys": "6.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.18.0.tgz", + "integrity": "sha512-o/UoDT2NgOJ2VfHpfr+KBY2ErWvCySNUIX/X7O9g8Zzt/tXdpfEU43qbNk8LVuWUT2E0ptzTWXh79i74PP0twA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.18.0", + "@typescript-eslint/visitor-keys": "6.18.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.18.0.tgz", + "integrity": "sha512-ZeMtrXnGmTcHciJN1+u2CigWEEXgy1ufoxtWcHORt5kGvpjjIlK9MUhzHm4RM8iVy6dqSaZA/6PVkX6+r+ChjQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.18.0", + "@typescript-eslint/utils": "6.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.18.0.tgz", + "integrity": "sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.18.0.tgz", + "integrity": "sha512-klNvl+Ql4NsBNGB4W9TZ2Od03lm7aGvTbs0wYaFYsplVPhr+oeXjlPZCDI4U9jgJIDK38W1FKhacCFzCC+nbIg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.18.0", + "@typescript-eslint/visitor-keys": "6.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.18.0.tgz", + "integrity": "sha512-wiKKCbUeDPGaYEYQh1S580dGxJ/V9HI7K5sbGAVklyf+o5g3O+adnS4UNJajplF4e7z2q0uVBaTdT/yLb4XAVA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.18.0", + "@typescript-eslint/types": "6.18.0", + "@typescript-eslint/typescript-estree": "6.18.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.18.0.tgz", + "integrity": "sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.18.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "dependencies": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-node/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-node/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-node/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", + "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/gts": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gts/-/gts-5.2.0.tgz", + "integrity": "sha512-25qOnePUUX7upFc4ycqWersDBq+o1X6hXUTW56JOWCxPYKJXQ1RWzqT9q+2SU3LfPKJf+4sz4Dw3VT0p96Kv6g==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "chalk": "^4.1.2", + "eslint": "8.50.0", + "eslint-config-prettier": "9.0.0", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "5.0.0", + "execa": "^5.0.0", + "inquirer": "^7.3.3", + "json5": "^2.1.3", + "meow": "^9.0.0", + "ncp": "^2.0.0", + "prettier": "3.0.3", + "rimraf": "3.0.2", + "write-file-atomic": "^4.0.0" + }, + "bin": { + "gts": "build/src/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "typescript": ">=3" + } + }, + "node_modules/gts/node_modules/@eslint/js": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/gts/node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/gts/node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { "typescript": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", - "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", - "dev": true + "optional": true + } + } + }, + "node_modules/gts/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } + }, + "node_modules/gts/node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/gts/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/gts/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/gts/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/gts/node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/gts/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/gts/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gts/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/gts/node_modules/eslint": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.50.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/gts/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/gts/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gts/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.1.0.tgz", + "integrity": "sha512-prmJlC1dbLhti4nE4XAPDWmfJesYO15sjGXVp7Cs7Ym5I9Xtwa/hUHxxJXjnpfLO72+ySttA0Ztf8g/RiVnUKw==", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "jasmine-core": "~5.1.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.1.tgz", + "integrity": "sha512-UrzO3fL7nnxlQXlvTynNAenL+21oUQRlzqQFsA2U11ryb4+NLOCOePZ70PTojEaUKhiFugh7dG0Q+I58xlPdWg==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "dev": true, + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/synckit/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } } } diff --git a/package.json b/package.json index 843e626..af7564f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "csp_evaluator", - "version": "1.0.4", + "version": "2.0.0", "description": "Evaluate Content Security Policies for a wide range of bypasses and weaknesses", "main": "dist/evaluator.js", "keywords": [ @@ -17,12 +17,27 @@ "type": "git", "url": "https://github.com/google/csp-evaluator" }, + "engines": { + "node": ">=10.0.0" + }, "scripts": { - "test": "tsc && npx jasmine --config=jasmine.json" + "test": "tsc && npx jasmine --config=jasmine.json", + "lint": "gts lint", + "clean": "gts clean", + "compile": "tsc", + "fix": "gts fix", + "prepare": "npm run compile", + "pretest": "npm run compile", + "posttest": "npm run lint" }, "devDependencies": { - "@types/jasmine": "^3.6.7", - "jasmine": "^3.7.0", - "typescript": "^4.2.3" + "@types/jasmine": "^5.1.4", + "@typescript-eslint/eslint-plugin": "^6.17.0", + "@typescript-eslint/parser": "^6.17.0", + "eslint": "^8.56.0", + "jasmine": "^5.1.0", + "typescript": "~5.2.0", + "gts": "^5.2.0", + "@types/node": "20.8.2" } } diff --git a/parser_test.ts b/parser_test.ts deleted file mode 100644 index 2a05673..0000000 --- a/parser_test.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @fileoverview Tests for CSP Parser. - * @author lwe@google.com (Lukas Weichselbaum) - * - * @license - * Copyright 2016 Google Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'jasmine'; - -import {CspParser, TEST_ONLY} from './parser'; - - -describe('Test parser', () => { - it('CspParser', () => { - const validCsp = // Test policy with different features from CSP2. - 'default-src \'none\';' + - 'script-src \'nonce-unsafefoobar\' \'unsafe-eval\' \'unsafe-inline\' \n' + - 'https://example.com/foo.js foo.bar; ' + - 'object-src \'none\';' + - 'img-src \'self\' https: data: blob:;' + - 'style-src \'self\' \'unsafe-inline\' \'sha256-1DCfk1NYWuHMfoobarfoobar=\';' + - 'font-src *;' + - 'child-src *.example.com:9090;' + - 'upgrade-insecure-requests;\n' + - 'report-uri /csp/test'; - - const parser = new (CspParser)(validCsp); - const parsedCsp = parser.csp; - - // check directives - const directives = Object.keys(parsedCsp.directives); - const expectedDirectives = [ - 'default-src', 'script-src', 'object-src', 'img-src', 'style-src', - 'font-src', 'child-src', 'upgrade-insecure-requests', 'report-uri' - ]; - expect(expectedDirectives) - .toEqual(jasmine.arrayWithExactContents(directives)); - - // check directive values - expect(['\'none\'']) - .toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['default-src'] as string[])); - - expect([ - '\'nonce-unsafefoobar\'', '\'unsafe-eval\'', '\'unsafe-inline\'', - 'https://example.com/foo.js', 'foo.bar' - ]) - .toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['script-src'] as string[])); - - expect(['\'none\'']) - .toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['object-src'] as string[])); - - expect(['\'self\'', 'https:', 'data:', 'blob:']) - .toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['img-src'] as string[])); - expect([ - '\'self\'', '\'unsafe-inline\'', '\'sha256-1DCfk1NYWuHMfoobarfoobar=\'' - ]) - .toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['style-src'] as string[])); - expect(['*']).toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['font-src'] as string[])); - expect(['*.example.com:9090']) - .toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['child-src'] as string[])); - expect([]).toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['upgrade-insecure-requests'] as string[])); - expect(['/csp/test']) - .toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['report-uri'] as string[])); - }); - - it('CspParserDuplicateDirectives', () => { - const validCsp = 'default-src \'none\';' + - 'default-src foo.bar;' + - 'object-src \'none\';' + - 'OBJECT-src foo.bar;'; - - const parser = new (CspParser)(validCsp); - const parsedCsp = parser.csp; - - // check directives - const directives = Object.keys(parsedCsp.directives); - const expectedDirectives = ['default-src', 'object-src']; - expect(expectedDirectives) - .toEqual(jasmine.arrayWithExactContents(directives)); - - // check directive values - expect(['\'none\'']) - .toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['default-src'] as string[])); - expect(['\'none\'']) - .toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['object-src'] as string[])); - }); - - it('CspParserMixedCaseKeywords', () => { - const validCsp = 'DEFAULT-src \'NONE\';' + // Keywords should be - // case insensetive. - 'img-src \'sElf\' HTTPS: Example.com/CaseSensitive;'; - - const parser = new (CspParser)(validCsp); - const parsedCsp = parser.csp; - - // check directives - const directives = Object.keys(parsedCsp.directives); - const expectedDirectives = ['default-src', 'img-src']; - expect(expectedDirectives) - .toEqual(jasmine.arrayWithExactContents(directives)); - - // check directive values - expect(['\'none\'']) - .toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['default-src'] as string[])); - expect(['\'self\'', 'https:', 'Example.com/CaseSensitive']) - .toEqual(jasmine.arrayWithExactContents( - parsedCsp.directives['img-src'] as string[])); - }); - - it('NormalizeDirectiveValue', () => { - expect(TEST_ONLY.normalizeDirectiveValue('\'nOnE\'')).toBe('\'none\''); - expect(TEST_ONLY.normalizeDirectiveValue('\'nonce-aBcD\'')) - .toBe('\'nonce-aBcD\''); - expect(TEST_ONLY.normalizeDirectiveValue('\'hash-XyZ==\'')) - .toBe('\'hash-XyZ==\''); - expect(TEST_ONLY.normalizeDirectiveValue('HTTPS:')).toBe('https:'); - expect(TEST_ONLY.normalizeDirectiveValue('example.com/TEST')) - .toBe('example.com/TEST'); - }); -}); diff --git a/allowlist_bypasses/angular.ts b/src/allowlist_bypasses/angular.ts similarity index 82% rename from allowlist_bypasses/angular.ts rename to src/allowlist_bypasses/angular.ts index 2eb45c0..1d2e4b5 100644 --- a/allowlist_bypasses/angular.ts +++ b/src/allowlist_bypasses/angular.ts @@ -17,7 +17,6 @@ * limitations under the License. */ - /** * Angular libraries on commonly allowlisted origins (e.g. CDNs) that would * allow a CSP bypass. @@ -32,38 +31,29 @@ export const URLS: string[] = [ '//yastatic.net/angularjs/1.2.23/angular.min.js', '//yuedust.yuedu.126.net/js/components/angular/angular.js', '//art.jobs.netease.com/script/angular.js', - '//csu-c45.kxcdn.com/angular/angular.js', '//elysiumwebsite.s3.amazonaws.com/uploads/blog-media/rockstar/angular.min.js', '//inno.blob.core.windows.net/new/libs/AngularJS/1.2.1/angular.min.js', '//gift-talk.kakao.com/public/javascripts/angular.min.js', '//ajax.googleapis.com/ajax/libs/angularjs/1.2.0rc1/angular-route.min.js', - '//master-sumok.ru/vendors/angular/angular-cookies.js', '//ayicommon-a.akamaihd.net/static/vendor/angular-1.4.2.min.js', - '//pangxiehaitao.com/framework/angular-1.3.9/angular-animate.min.js', '//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.16/angular.min.js', '//96fe3ee995e96e922b6b-d10c35bd0a0de2c718b252bc575fdb73.ssl.cf1.rackcdn.com/angular.js', '//oss.maxcdn.com/angularjs/1.2.20/angular.min.js', - '//reports.zemanta.com/smedia/common/angularjs/1.2.11/angular.js', '//cdn.shopify.com/s/files/1/0225/6463/t/1/assets/angular-animate.min.js', '//parademanagement.com.s3-website-ap-southeast-1.amazonaws.com/js/angular.min.js', '//cdn.jsdelivr.net/angularjs/1.1.2/angular.min.js', '//eb2883ede55c53e09fd5-9c145fb03d93709ea57875d307e2d82e.ssl.cf3.rackcdn.com/components/angular-resource.min.js', - '//andors-trail.googlecode.com/git/AndorsTrailEdit/lib/angular.min.js', '//cdn.walkme.com/General/EnvironmentTests/angular/angular.min.js', '//laundrymail.com/angular/angular.js', '//s3-eu-west-1.amazonaws.com/staticancpa/js/angular-cookies.min.js', '//collade.demo.stswp.com/js/vendor/angular.min.js', '//mrfishie.github.io/sailor/bower_components/angular/angular.min.js', - '//askgithub.com/static/js/angular.min.js', '//services.amazon.com/solution-providers/assets/vendor/angular-cookies.min.js', '//raw.githubusercontent.com/angular/code.angularjs.org/master/1.0.7/angular-resource.js', '//prb-resume.appspot.com/bower_components/angular-animate/angular-animate.js', - '//dl.dropboxusercontent.com/u/30877786/angular.min.js', '//static.tumblr.com/x5qdx0r/nPOnngtff/angular-resource.min_1_.js', - '//storage.googleapis.com/assets-prod.urbansitter.net/us-sym/assets/vendor/angular-sanitize/angular-sanitize.min.js', '//twitter.github.io/labella.js/bower_components/angular/angular.min.js', '//cdn2-casinoroom.global.ssl.fastly.net/js/lib/angular-animate.min.js', '//www.adobe.com/devnet-apps/flashshowcase/lib/angular/angular.1.1.5.min.js', - '//eternal-sunset.herokuapp.com/bower_components/angular/angular.js', - '//cdn.bootcss.com/angular.js/1.2.0/angular.min.js' + '//cdn.bootcss.com/angular.js/1.2.0/angular.min.js', ]; diff --git a/allowlist_bypasses/flash.ts b/src/allowlist_bypasses/flash.ts similarity index 96% rename from allowlist_bypasses/flash.ts rename to src/allowlist_bypasses/flash.ts index 58ca769..9088fd7 100644 --- a/allowlist_bypasses/flash.ts +++ b/src/allowlist_bypasses/flash.ts @@ -18,13 +18,11 @@ * limitations under the License. */ - /** * Domains that would allow a CSP bypass if allowlisted. * Only most common paths will be listed here. Hence there might still be other * paths on these domains that would allow a bypass. */ export const URLS: string[] = [ - '//vk.com/swf/video.swf', - '//ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/charts/assets/charts.swf' + '//ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/charts/assets/charts.swf', ]; diff --git a/allowlist_bypasses/json/angular.json b/src/allowlist_bypasses/json/angular.json similarity index 78% rename from allowlist_bypasses/json/angular.json rename to src/allowlist_bypasses/json/angular.json index 98fb36f..35c25bc 100644 --- a/allowlist_bypasses/json/angular.json +++ b/src/allowlist_bypasses/json/angular.json @@ -7,39 +7,30 @@ "//yastatic.net/angularjs/1.2.23/angular.min.js", "//yuedust.yuedu.126.net/js/components/angular/angular.js", "//art.jobs.netease.com/script/angular.js", - "//csu-c45.kxcdn.com/angular/angular.js", "//elysiumwebsite.s3.amazonaws.com/uploads/blog-media/rockstar/angular.min.js", "//inno.blob.core.windows.net/new/libs/AngularJS/1.2.1/angular.min.js", "//gift-talk.kakao.com/public/javascripts/angular.min.js", "//ajax.googleapis.com/ajax/libs/angularjs/1.2.0rc1/angular-route.min.js", - "//master-sumok.ru/vendors/angular/angular-cookies.js", "//ayicommon-a.akamaihd.net/static/vendor/angular-1.4.2.min.js", - "//pangxiehaitao.com/framework/angular-1.3.9/angular-animate.min.js", "//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.16/angular.min.js", "//96fe3ee995e96e922b6b-d10c35bd0a0de2c718b252bc575fdb73.ssl.cf1.rackcdn.com/angular.js", "//oss.maxcdn.com/angularjs/1.2.20/angular.min.js", - "//reports.zemanta.com/smedia/common/angularjs/1.2.11/angular.js", "//cdn.shopify.com/s/files/1/0225/6463/t/1/assets/angular-animate.min.js", "//parademanagement.com.s3-website-ap-southeast-1.amazonaws.com/js/angular.min.js", "//cdn.jsdelivr.net/angularjs/1.1.2/angular.min.js", "//eb2883ede55c53e09fd5-9c145fb03d93709ea57875d307e2d82e.ssl.cf3.rackcdn.com/components/angular-resource.min.js", - "//andors-trail.googlecode.com/git/AndorsTrailEdit/lib/angular.min.js", "//cdn.walkme.com/General/EnvironmentTests/angular/angular.min.js", "//laundrymail.com/angular/angular.js", "//s3-eu-west-1.amazonaws.com/staticancpa/js/angular-cookies.min.js", "//collade.demo.stswp.com/js/vendor/angular.min.js", "//mrfishie.github.io/sailor/bower_components/angular/angular.min.js", - "//askgithub.com/static/js/angular.min.js", "//services.amazon.com/solution-providers/assets/vendor/angular-cookies.min.js", "//raw.githubusercontent.com/angular/code.angularjs.org/master/1.0.7/angular-resource.js", "//prb-resume.appspot.com/bower_components/angular-animate/angular-animate.js", - "//dl.dropboxusercontent.com/u/30877786/angular.min.js", "//static.tumblr.com/x5qdx0r/nPOnngtff/angular-resource.min_1_.js", - "//storage.googleapis.com/assets-prod.urbansitter.net/us-sym/assets/vendor/angular-sanitize/angular-sanitize.min.js", "//twitter.github.io/labella.js/bower_components/angular/angular.min.js", "//cdn2-casinoroom.global.ssl.fastly.net/js/lib/angular-animate.min.js", "//www.adobe.com/devnet-apps/flashshowcase/lib/angular/angular.1.1.5.min.js", - "//eternal-sunset.herokuapp.com/bower_components/angular/angular.js", "//cdn.bootcss.com/angular.js/1.2.0/angular.min.js" ] } diff --git a/allowlist_bypasses/json/flash.json b/src/allowlist_bypasses/json/flash.json similarity index 77% rename from allowlist_bypasses/json/flash.json rename to src/allowlist_bypasses/json/flash.json index 1a8a7c5..0f41206 100644 --- a/allowlist_bypasses/json/flash.json +++ b/src/allowlist_bypasses/json/flash.json @@ -1,6 +1,5 @@ {"urls": [ - "//vk.com/swf/video.swf", "//ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/charts/assets/charts.swf" ] } diff --git a/allowlist_bypasses/json/jsonp.json b/src/allowlist_bypasses/json/jsonp.json similarity index 97% rename from allowlist_bypasses/json/jsonp.json rename to src/allowlist_bypasses/json/jsonp.json index d069eb6..17ac33e 100644 --- a/allowlist_bypasses/json/jsonp.json +++ b/src/allowlist_bypasses/json/jsonp.json @@ -37,18 +37,15 @@ "//mc.yandex.ru/watch/24306916/1", "//share.yandex.net/counter/gpp/", "//ok.go.mail.ru/lady_on_lady_recipes_r.json", - "//d1f69o4buvlrj5.cloudfront.net/__efa_15_1_ornpba.xekq.arg/optout_check", "//www.googletagmanager.com/gtm/js", "//api.vk.com/method/wall.get", "//www.sharethis.com/get-publisher-info.php", "//google.ru/maps/vt", "//pro.netrox.sc/oapi/h_checksite.ashx", "//vimeo.com/api/oembed.json/", - "//de.blog.newrelic.com/wp-admin/admin-ajax.php", "//ajax.googleapis.com/ajax/services/search/news", "//ssl.google-analytics.com/gtm/js", "//pubsub.pubnub.com/subscribe/demo/hello_world/", - "//pass.yandex.ua/services", "//id.rambler.ru/script/topline_info.js", "//m.addthis.com/live/red_lojson/100eng.json", "//passport.ngs.ru/ajax/check", diff --git a/allowlist_bypasses/jsonp.ts b/src/allowlist_bypasses/jsonp.ts similarity index 95% rename from allowlist_bypasses/jsonp.ts rename to src/allowlist_bypasses/jsonp.ts index 5caa7ad..a9dc4f2 100644 --- a/allowlist_bypasses/jsonp.ts +++ b/src/allowlist_bypasses/jsonp.ts @@ -23,19 +23,18 @@ * limitations under the License. */ - /** * Some JSONP-like bypasses only work if the CSP allows 'eval()'. */ export const NEEDS_EVAL: string[] = [ - 'googletagmanager.com', 'www.googletagmanager.com', - - 'www.googleadservices.com', 'google-analytics.com', - 'ssl.google-analytics.com', 'www.google-analytics.com' + 'googletagmanager.com', + 'www.googletagmanager.com', + 'www.googleadservices.com', + 'google-analytics.com', + 'ssl.google-analytics.com', + 'www.google-analytics.com', ]; - - /** * JSONP endpoints on commonly allowlisted origins (e.g. CDNs) that would allow * a CSP bypass. @@ -70,18 +69,15 @@ export const URLS: string[] = [ '//mc.yandex.ru/watch/24306916/1', '//share.yandex.net/counter/gpp/', '//ok.go.mail.ru/lady_on_lady_recipes_r.json', - '//d1f69o4buvlrj5.cloudfront.net/__efa_15_1_ornpba.xekq.arg/optout_check', '//www.googletagmanager.com/gtm/js', '//api.vk.com/method/wall.get', '//www.sharethis.com/get-publisher-info.php', '//google.ru/maps/vt', '//pro.netrox.sc/oapi/h_checksite.ashx', '//vimeo.com/api/oembed.json/', - '//de.blog.newrelic.com/wp-admin/admin-ajax.php', '//ajax.googleapis.com/ajax/services/search/news', '//ssl.google-analytics.com/gtm/js', '//pubsub.pubnub.com/subscribe/demo/hello_world/', - '//pass.yandex.ua/services', '//id.rambler.ru/script/topline_info.js', '//m.addthis.com/live/red_lojson/100eng.json', '//passport.ngs.ru/ajax/check', @@ -165,5 +161,5 @@ export const URLS: string[] = [ '//maps.google.ru/maps/vt', '//c1n2.hypercomments.com/stream/subscribe', '//rec.ydf.yandex.ru/cookie', - '//cdn.jsdelivr.net' + '//cdn.jsdelivr.net', ]; diff --git a/checks/checker.ts b/src/checks/checker.ts similarity index 74% rename from checks/checker.ts rename to src/checks/checker.ts index 3cda548..9783cf9 100644 --- a/checks/checker.ts +++ b/src/checks/checker.ts @@ -6,7 +6,7 @@ import {Csp} from '../csp'; import {Finding} from '../finding'; /** - * A function that checks a given Csp for problems and returns an unordered + * A function that checks a list of Csps for problems and returns an unordered * list of Findings. */ export type CheckerFunction = (csp: Csp) => Finding[]; diff --git a/src/checks/parser_checks.ts b/src/checks/parser_checks.ts new file mode 100644 index 0000000..ce73911 --- /dev/null +++ b/src/checks/parser_checks.ts @@ -0,0 +1,192 @@ +/** + * @fileoverview Collection of CSP parser checks which can be used to find + * common syntax mistakes like missing semicolons, invalid directives or + * invalid keywords. + * @author lwe@google.com (Lukas Weichselbaum) + * + * @license + * Copyright 2016 Google Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Csp, + Directive, + Keyword, + TrustedTypesSink, + isDirective, + isHash, + isKeyword, + isNonce, +} from '../csp'; + +import {Finding, Severity, Type} from '../finding'; + +/** + * Checks if the csp contains invalid directives. + * + * Example policy where this check would trigger: + * foobar-src foo.bar + * + * @param parsedCsp A parsed csp. + */ +export function checkUnknownDirective(parsedCsps: Csp): Finding[] { + const findings: Finding[] = []; + + for (const currentCsp of parsedCsps.directives) { + for (const directive of Object.keys(currentCsp)) { + if (isDirective(directive)) { + // Directive is known. + continue; + } + + if (directive.endsWith(':')) { + findings.push( + new Finding( + Type.UNKNOWN_DIRECTIVE, + "CSP directives don't end with a colon.", + Severity.SYNTAX, + directive + ) + ); + } else { + findings.push( + new Finding( + Type.UNKNOWN_DIRECTIVE, + 'Directive "' + directive + '" is not a known CSP directive.', + Severity.SYNTAX, + directive + ) + ); + } + } + } + + return findings; +} + +/** + * Checks if semicolons are missing in the csp. + * + * Example policy where this check would trigger (missing semicolon before + * start of object-src): + * script-src foo.bar object-src 'none' + * + * @param parsedCsp A parsed csp. + */ +export function checkMissingSemicolon(parsedCsps: Csp): Finding[] { + const findings: Finding[] = []; + + for (const cspChecked of parsedCsps.directives) { + for (const [directive, directiveValues] of Object.entries(cspChecked)) { + if (directiveValues === undefined) { + continue; + } + for (const value of directiveValues) { + // If we find a known directive inside a directive value, it is very + // likely that a semicolon was forgoten. + if (isDirective(value)) { + findings.push( + new Finding( + Type.MISSING_SEMICOLON, + 'Did you forget the semicolon? ' + + '"' + + value + + '" seems to be a directive, not a value.', + Severity.SYNTAX, + directive, + value + ) + ); + } + } + } + } + + return findings; +} + +/** + * Checks if csp contains invalid keywords. + * + * Example policy where this check would trigger: + * script-src 'notAkeyword' + * + * @param parsedCsp A parsed csp. + */ +export function checkInvalidKeyword(parsedCsps: Csp): Finding[] { + const findings: Finding[] = []; + const keywordsNoTicks = Object.values(Keyword).map(k => k.replace(/'/g, '')); + + for (const cspChecked of parsedCsps.directives) { + for (const [directive, directiveValues] of Object.entries(cspChecked)) { + if (directiveValues === undefined) { + continue; + } + for (const value of directiveValues) { + // Check if single ticks have been forgotten. + if ( + keywordsNoTicks.some(k => k === value) || + value.startsWith('nonce-') || + value.match(/^(sha256|sha384|sha512)-/) + ) { + findings.push( + new Finding( + Type.INVALID_KEYWORD, + 'Did you forget to surround "' + value + '" with single-ticks?', + Severity.SYNTAX, + directive, + value + ) + ); + continue; + } + + // Continue, if the value doesn't start with single tick. + // All CSP keywords start with a single tick. + if (!value.startsWith("'")) { + continue; + } + + if (directive === Directive.REQUIRE_TRUSTED_TYPES_FOR) { + // Continue, if it's an allowed Trusted Types sink. + if (value === TrustedTypesSink.SCRIPT) { + continue; + } + } else if (directive === Directive.TRUSTED_TYPES) { + // Continue, if it's an allowed Trusted Types keyword. + if (value === "'allow-duplicates'" || value === "'none'") { + continue; + } + } else { + // Continue, if it's a valid keyword. + if (isKeyword(value) || isHash(value) || isNonce(value)) { + continue; + } + } + + findings.push( + new Finding( + Type.INVALID_KEYWORD, + value + ' seems to be an invalid CSP keyword.', + Severity.SYNTAX, + directive, + value + ) + ); + } + } + } + + return findings; +} diff --git a/src/checks/security_checks.ts b/src/checks/security_checks.ts new file mode 100644 index 0000000..107a70a --- /dev/null +++ b/src/checks/security_checks.ts @@ -0,0 +1,912 @@ +/** + * @fileoverview Collection of CSP evaluation checks. + * @author lwe@google.com (Lukas Weichselbaum) + * + * @license + * Copyright 2016 Google Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as angular from '../allowlist_bypasses/angular'; +import * as flash from '../allowlist_bypasses/flash'; +import * as jsonp from '../allowlist_bypasses/jsonp'; +import {Csp, Directive, Keyword, isNonce, isUrlScheme} from '../csp'; +import {Finding, Severity, Type} from '../finding'; +import * as utils from '../utils'; + +/** + * A list of CSP directives that can allow XSS vulnerabilities if they fail + * validation. + */ +export const DIRECTIVES_CAUSING_XSS: Directive[] = [ + Directive.SCRIPT_SRC, + Directive.OBJECT_SRC, + Directive.BASE_URI, +]; + +/** + * A list of URL schemes that can allow XSS vulnerabilities when requests to + * them are made. + */ +export const URL_SCHEMES_CAUSING_XSS: string[] = ['data:', 'http:', 'https:']; + +/** + * Checks if passed csp allows inline scripts. + * Findings of this check are critical and FP free. + * unsafe-inline is ignored in the presence of a nonce or a hash. This check + * does not account for this and therefore the effectiveCsp needs to be passed. + * + * Example policy where this check would trigger: + * script-src 'unsafe-inline' + * + * @param effectiveCsp A parsed csp that only contains values which + * are active in a certain version of CSP (e.g. no unsafe-inline if a nonce + * is present). + */ +export function checkScriptUnsafeInline(effectiveCsp: Csp): Finding[] { + const findings: Finding[] = []; + + const directiveName = effectiveCsp.getEffectiveDirective( + Directive.SCRIPT_SRC + ); + for (const cspChecked of effectiveCsp.directives) { + const values: string[] = cspChecked[directiveName] || []; + + // Check if unsafe-inline is present. + if (values.includes(Keyword.UNSAFE_INLINE)) { + findings.push( + new Finding( + Type.SCRIPT_UNSAFE_INLINE, + "'unsafe-inline' allows the execution of unsafe in-page scripts " + + 'and event handlers.', + Severity.HIGH, + directiveName, + Keyword.UNSAFE_INLINE + ) + ); + } + } + + return findings; +} + +/** + * Checks if passed csp allows inline styles. + * Findings of this check are critical and FP free. + * unsafe-inline is ignored in the presence of a nonce or a hash. This check + * does not account for this and therefore the effectiveCsp needs to be passed. + * + * Example policy where this check would trigger: + * style-src 'unsafe-inline' + * + * @param effectiveCsp A parsed csp that only contains values which + * are active in a certain version of CSP (e.g. no unsafe-inline if a nonce + * is present). + */ +export function checkStyleUnsafeInline(effectiveCsp: Csp): Finding[] { + const findings: Finding[] = []; + + const directiveName = effectiveCsp.getEffectiveDirective(Directive.STYLE_SRC); + for (const cspChecked of effectiveCsp.directives) { + const values: string[] = cspChecked[directiveName] || []; + + // Check if unsafe-inline is present. + if (values.includes(Keyword.UNSAFE_INLINE)) { + findings.push( + new Finding( + Type.STYLE_UNSAFE_INLINE, + 'Unsafe inline stylesheet are allowed. This could help social engineering attacks in case of content injection.', + Severity.MEDIUM, + directiveName, + Keyword.UNSAFE_INLINE + ) + ); + } + } + + return findings; +} + +/** + * Checks if passed csp allows web assembly eval in scripts. + * Findings of this check have a medium severity and are FP free. + * + * Example policy where this check would trigger: + * script-src 'wasm-unsafe-eval' + * + * @param parsedCsp Parsed CSP. + */ +export function checkScriptWasmUnsafeEval(parsedCsps: Csp): Finding[] { + const findings: Finding[] = []; + + const directiveName = parsedCsps.getEffectiveDirective(Directive.SCRIPT_SRC); + for (const cspChecked of parsedCsps.directives) { + const values: string[] = cspChecked[directiveName] || []; + + // Check if wasm-unsafe-eval is present. + if (values.includes(Keyword.WASM_UNSAFE_EVAL)) { + findings.push( + new Finding( + Type.SCRIPT_WASM_UNSAFE_EVAL, + "'wasm-unsafe-eval' allows the execution of web assembly code injected in functions " + + 'such as WebAssembly.compile().', + Severity.MEDIUM_MAYBE, + directiveName, + Keyword.UNSAFE_EVAL + ) + ); + } + } + + return findings; +} + +/** + * Checks if passed csp allows eval in scripts. + * Findings of this check have a medium severity and are FP free. + * + * Example policy where this check would trigger: + * script-src 'unsafe-eval' + * + * @param parsedCsp Parsed CSP. + */ +export function checkScriptUnsafeEval(parsedCsps: Csp): Finding[] { + const directiveName = parsedCsps.getEffectiveDirective(Directive.SCRIPT_SRC); + for (const cspChecked of parsedCsps.directives) { + const values: string[] = cspChecked[directiveName] || []; + + // Check if unsafe-eval is present. + if (values.includes(Keyword.UNSAFE_EVAL)) { + return [ + new Finding( + Type.SCRIPT_UNSAFE_EVAL, + "'unsafe-eval' allows the execution of code injected into DOM APIs " + + 'such as eval().', + Severity.MEDIUM_MAYBE, + directiveName, + Keyword.UNSAFE_EVAL + ), + ]; + } + } + return []; +} + +/** + * Checks if plain URL schemes (e.g. http:) are allowed in sensitive directives. + * Findings of this check have a high severity and are FP free. + * + * Example policy where this check would trigger: + * script-src https: http: data: + * + * @param parsedCsp Parsed CSP. + */ +export function checkPlainUrlSchemes(parsedCsps: Csp): Finding[] { + const violations: Finding[] = []; + + const directivesToCheck = parsedCsps.getEffectiveDirectives( + DIRECTIVES_CAUSING_XSS + ); + for (const cspChecked of parsedCsps.directives) { + for (const directive of directivesToCheck) { + const values = cspChecked[directive] || []; + for (const value of values) { + if (URL_SCHEMES_CAUSING_XSS.includes(value)) { + violations.push( + new Finding( + Type.PLAIN_URL_SCHEMES, + value + + ' URI in ' + + directive + + ' allows the execution of ' + + 'unsafe scripts.', + Severity.HIGH, + directive, + value + ) + ); + } + } + } + } + + return violations; +} + +/** + * Checks if plain URL schemes (e.g. http:) are allowed in the form-action directive. + * Findings of this check have a low severity and are FP free. + * + * Example policy where this check would trigger: + * form-action https: http: + * + * @param parsedCsp Parsed CSP. + */ +export function checkPlainUrlSchemesInFormActions(parsedCsps: Csp): Finding[] { + const violations: Finding[] = []; + + for (const cspChecked of parsedCsps.directives) { + const values = cspChecked[Directive.FORM_ACTION] || []; + for (const value of values) { + if (value === 'https:' || value === 'http:') { + violations.push( + new Finding( + Type.PLAIN_URL_SCHEMES, + 'Form actions only has protocol restrictions and still allows arbitrary hosts. This could help social engineering attacks in case of content injection.', + Severity.LOW, + Directive.FORM_ACTION, + value + ) + ); + } + } + } + + return violations; +} + +/** + * Checks if csp contains wildcards in sensitive directives. + * Findings of this check have a high severity and are FP free. + * + * Example policy where this check would trigger: + * script-src * + * + * @param parsedCsp Parsed CSP. + */ +export function checkWildcards(parsedCsps: Csp): Finding[] { + const violations: Finding[] = []; + + const directivesToCheck = parsedCsps.getEffectiveDirectives( + DIRECTIVES_CAUSING_XSS + ); + for (const cspChecked of parsedCsps.directives) { + for (const directive of directivesToCheck) { + const values = cspChecked[directive] || []; + for (const value of values) { + const url = utils.getSchemeFreeUrl(value); + if (url === '*') { + violations.push( + new Finding( + Type.PLAIN_WILDCARD, + directive + " should not allow '*' as source", + Severity.HIGH, + directive, + value + ) + ); + continue; + } + } + } + } + + return violations; +} + +/** + * Checks if object-src is restricted to none either directly or via a + * default-src. + */ +export function checkMissingObjectSrcDirective(parsedCsps: Csp): Finding[] { + let objectRestrictions: string[] | undefined = []; + + for (const cspChecked of parsedCsps.directives) { + if (Directive.OBJECT_SRC in cspChecked) { + objectRestrictions = cspChecked[Directive.OBJECT_SRC]; + } else if (Directive.DEFAULT_SRC in cspChecked) { + objectRestrictions = cspChecked[Directive.DEFAULT_SRC]; + } + } + + if (objectRestrictions !== undefined && objectRestrictions.length >= 1) { + return []; + } + return [ + new Finding( + Type.MISSING_DIRECTIVES, + "Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?", + Severity.HIGH, + Directive.OBJECT_SRC + ), + ]; +} + +/** + * Checks if script-src is restricted either directly or via a default-src. + */ +export function checkMissingScriptSrcDirective(parsedCsps: Csp): Finding[] { + for (const cspChecked of parsedCsps.directives) { + if ( + Directive.SCRIPT_SRC in cspChecked || + Directive.DEFAULT_SRC in cspChecked + ) { + return []; + } + } + return [ + new Finding( + Type.MISSING_DIRECTIVES, + 'script-src directive is missing.', + Severity.HIGH, + Directive.SCRIPT_SRC + ), + ]; +} + +/** + * Checks if style-src is restricted either directly or via a default-src. + */ +export function checkMissingStyleSrcDirective(parsedCsps: Csp): Finding[] { + for (const cspChecked of parsedCsps.directives) { + if ( + Directive.STYLE_SRC in cspChecked || + Directive.DEFAULT_SRC in cspChecked + ) { + return []; + } + } + return [ + new Finding( + Type.MISSING_DIRECTIVES, + 'No stylesheet restrictions are present. This could help social engineering attacks in case of content injection.', + Severity.LOW, + Directive.STYLE_SRC + ), + ]; +} + +/** + * Checks if form-action is restricted. + */ +export function checkMissingFormActionDirective(parsedCsps: Csp): Finding[] { + for (const cspChecked of parsedCsps.directives) { + if (Directive.FORM_ACTION in cspChecked) { + return []; + } + } + return [ + new Finding( + Type.MISSING_DIRECTIVES, + 'No form action restrictions are present. This could help social engineering attacks in case of content injection.', + Severity.LOW, + Directive.FORM_ACTION + ), + ]; +} + +/** + * Checks if the base-uri needs to be restricted and if so, whether it has been + * restricted. + */ +export function checkMissingBaseUriDirective(parsedCsps: Csp): Finding[] { + // base-uri can be used to bypass nonce based CSPs and hash based CSPs that + // use strict dynamic + const needsBaseUri = + parsedCsps.policyHasScriptNonces() || + (parsedCsps.policyHasScriptHashes() && parsedCsps.policyHasStrictDynamic()); + + if (needsBaseUri) { + let hasBaseUri: boolean = false; + + for (const currentCsp of parsedCsps.directives) { + if (Directive.BASE_URI in currentCsp) { + hasBaseUri = true; + } + } + + if (!hasBaseUri) { + const description = + 'Missing base-uri allows the injection of base tags. ' + + 'They can be used to set the base URL for all relative (script) ' + + 'URLs to an attacker controlled domain. ' + + "Can you set it to 'none' or 'self'?"; + return [ + new Finding( + Type.MISSING_DIRECTIVES, + description, + Severity.HIGH, + Directive.BASE_URI + ), + ]; + } + } + + return []; +} + +/** + * Checks if all necessary directives for preventing XSS are set. + * Findings of this check have a high severity and are FP free. + * + * Example policy where this check would trigger: + * script-src 'none' + * + * @param parsedCsp Parsed CSP. + */ +export function checkMissingDirectives(parsedCsps: Csp): Finding[] { + return [ + ...checkMissingObjectSrcDirective(parsedCsps), + ...checkMissingScriptSrcDirective(parsedCsps), + ...checkMissingBaseUriDirective(parsedCsps), + ...checkMissingStyleSrcDirective(parsedCsps), + ...checkMissingFormActionDirective(parsedCsps), + ]; +} + +/** + * Checks if allowlisted origins are bypassable by JSONP/Angular endpoints. + * High severity findings of this check are FP free. + * + * Example policy where this check would trigger: + * default-src 'none'; script-src www.google.com + * + * @param parsedCsp Parsed CSP. + */ +export function checkScriptAllowlistBypass(parsedCsps: Csp): Finding[] { + const violations: Finding[] = []; + + const effectiveScriptSrcDirective = parsedCsps.getEffectiveDirective( + Directive.SCRIPT_SRC + ); + for (const cspChecked of parsedCsps.directives) { + const scriptSrcValues = cspChecked[effectiveScriptSrcDirective] || []; + if (scriptSrcValues.includes(Keyword.NONE)) { + return violations; + } + + for (const value of scriptSrcValues) { + if (value === Keyword.SELF) { + violations.push( + new Finding( + Type.SCRIPT_ALLOWLIST_BYPASS, + "'self' can be problematic if you host JSONP, AngularJS or user " + + 'uploaded files.', + Severity.MEDIUM_MAYBE, + effectiveScriptSrcDirective, + value + ) + ); + continue; + } + + // Ignore keywords, nonces and hashes (they start with a single quote). + if (value.startsWith("'")) { + continue; + } + + // Ignore standalone schemes and things that don't look like URLs (no dot). + if (isUrlScheme(value) || value.indexOf('.') === -1) { + continue; + } + + const url = '//' + utils.getSchemeFreeUrl(value); + + const angularBypass = utils.matchWildcardUrls(url, angular.URLS); + + let jsonpBypass = utils.matchWildcardUrls(url, jsonp.URLS); + + // Some JSONP bypasses only work in presence of unsafe-eval. + if (jsonpBypass) { + const evalRequired = jsonp.NEEDS_EVAL.includes(jsonpBypass.hostname); + const evalPresent = scriptSrcValues.includes(Keyword.UNSAFE_EVAL); + if (evalRequired && !evalPresent) { + jsonpBypass = null; + } + } + + if (jsonpBypass || angularBypass) { + let bypassDomain = ''; + let bypassTxt = ''; + if (jsonpBypass) { + bypassDomain = jsonpBypass.hostname; + bypassTxt = ' JSONP endpoints'; + } + if (angularBypass) { + bypassDomain = angularBypass.hostname; + bypassTxt += bypassTxt.trim() === '' ? '' : ' and'; + bypassTxt += ' Angular libraries'; + } + + violations.push( + new Finding( + Type.SCRIPT_ALLOWLIST_BYPASS, + bypassDomain + + ' is known to host' + + bypassTxt + + ' which allow to bypass this CSP.', + Severity.HIGH, + effectiveScriptSrcDirective, + value + ) + ); + } else { + violations.push( + new Finding( + Type.SCRIPT_ALLOWLIST_BYPASS, + "No bypass found; make sure that this URL doesn't serve JSONP " + + 'replies or Angular libraries.', + Severity.MEDIUM_MAYBE, + effectiveScriptSrcDirective, + value + ) + ); + } + } + } + + return violations; +} + +/** + * Checks if allowlisted object-src origins are bypassable. + * Findings of this check have a high severity and are FP free. + * + * Example policy where this check would trigger: + * default-src 'none'; object-src ajax.googleapis.com + * + * @param parsedCsp Parsed CSP. + */ +export function checkFlashObjectAllowlistBypass(parsedCsps: Csp): Finding[] { + const violations = []; + + const effectiveObjectSrcDirective = parsedCsps.getEffectiveDirective( + Directive.OBJECT_SRC + ); + for (const cspChecked of parsedCsps.directives) { + const objectSrcValues = cspChecked[effectiveObjectSrcDirective] || []; + + // If flash is not allowed in plugin-types, continue. + const pluginTypes = cspChecked[Directive.PLUGIN_TYPES]; + if (pluginTypes && !pluginTypes.includes('application/x-shockwave-flash')) { + return []; + } + + for (const value of objectSrcValues) { + // Nothing to do here if 'none'. + if (value === Keyword.NONE) { + return []; + } + + const url = '//' + utils.getSchemeFreeUrl(value); + const flashBypass = utils.matchWildcardUrls(url, flash.URLS); + + if (flashBypass) { + violations.push( + new Finding( + Type.OBJECT_ALLOWLIST_BYPASS, + flashBypass.hostname + + ' is known to host Flash files which allow to bypass this CSP.', + Severity.HIGH, + effectiveObjectSrcDirective, + value + ) + ); + } else if (effectiveObjectSrcDirective === Directive.OBJECT_SRC) { + violations.push( + new Finding( + Type.OBJECT_ALLOWLIST_BYPASS, + "Can you restrict object-src to 'none' only?", + Severity.MEDIUM_MAYBE, + effectiveObjectSrcDirective, + value + ) + ); + } + } + } + + return violations; +} + +/** + * Returns whether the given string "looks" like an IP address. This function + * only uses basic heuristics and does not accept all valid IPs nor reject all + * invalid IPs. + */ +export function looksLikeIpAddress(maybeIp: string): boolean { + if (maybeIp.startsWith('[') && maybeIp.endsWith(']')) { + // Looks like an IPv6 address and not a hostname (though it may be some + // nonsense like `[foo]`) + return true; + } + if (/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(maybeIp)) { + // Looks like an IPv4 address (though it may be something like + // `500.600.700.800` + return true; + } + // Won't match IP addresses encoded in other manners (eg octal or + // decimal) + return false; +} + +/** + * Checks if csp contains IP addresses. + * Findings of this check are informal only and are FP free. + * + * Example policy where this check would trigger: + * script-src 127.0.0.1 + * + * @param parsedCsp Parsed CSP. + */ +export function checkIpSource(parsedCsps: Csp): Finding[] { + const violations: Finding[] = []; + + for (const cspChecked of parsedCsps.directives) { + // Function for checking if directive values contain IP addresses. + const checkIp = (directive: string, directiveValues: string[]) => { + for (const value of directiveValues) { + const host = utils.getHostname(value); + if (looksLikeIpAddress(host)) { + // Check if localhost. + // See 4.8 in https://www.w3.org/TR/CSP2/#match-source-expression + if (host === '127.0.0.1') { + violations.push( + new Finding( + Type.IP_SOURCE, + directive + + ' directive allows localhost as source. ' + + 'Please make sure to remove this in production environments.', + Severity.INFO, + directive, + value + ) + ); + } else { + violations.push( + new Finding( + Type.IP_SOURCE, + directive + + ' directive has an IP-Address as source: ' + + host + + ' (will be ignored by browsers!). ', + Severity.INFO, + directive, + value + ) + ); + } + } + } + }; + + // Apply check to values of all directives. + utils.applyCheckFunktionToDirectives(cspChecked, checkIp); + } + return violations; +} + +/** + * Checks if csp contains directives that are deprecated in CSP3. + * Findings of this check are informal only and are FP free. + * + * Example policy where this check would trigger: + * report-uri foo.bar/csp + * + * @param parsedCsp Parsed CSP. + */ +export function checkDeprecatedDirective(parsedCsps: Csp): Finding[] { + const violations = []; + + for (const cspChecked of parsedCsps.directives) { + // More details: https://www.chromestatus.com/feature/5769374145183744 + if (Directive.REFLECTED_XSS in cspChecked) { + violations.push( + new Finding( + Type.DEPRECATED_DIRECTIVE, + 'reflected-xss is deprecated since CSP2. ' + + 'Please, use the X-XSS-Protection header instead.', + Severity.INFO, + Directive.REFLECTED_XSS + ) + ); + } + + // More details: https://www.chromestatus.com/feature/5680800376815616 + if (Directive.REFERRER in cspChecked) { + violations.push( + new Finding( + Type.DEPRECATED_DIRECTIVE, + 'referrer is deprecated since CSP2. ' + + 'Please, use the Referrer-Policy header instead.', + Severity.INFO, + Directive.REFERRER + ) + ); + } + + // More details: https://github.com/w3c/webappsec-csp/pull/327 + if (Directive.DISOWN_OPENER in cspChecked) { + violations.push( + new Finding( + Type.DEPRECATED_DIRECTIVE, + 'disown-opener is deprecated since CSP3. ' + + 'Please, use the Cross Origin Opener Policy header instead.', + Severity.INFO, + Directive.DISOWN_OPENER + ) + ); + } + } + return violations; +} + +/** + * Checks if csp nonce is at least 8 characters long. + * Findings of this check are of medium severity and are FP free. + * + * Example policy where this check would trigger: + * script-src 'nonce-short' + * + * @param parsedCsp Parsed CSP. + */ +export function checkNonceLength(parsedCsps: Csp): Finding[] { + const noncePattern = new RegExp("^'nonce-(.+)'$"); + const violations: Finding[] = []; + + for (const cspChecked of parsedCsps.directives) { + utils.applyCheckFunktionToDirectives( + cspChecked, + (directive, directiveValues) => { + for (const value of directiveValues) { + const match = value.match(noncePattern); + if (!match) { + continue; + } + // Not a nonce. + + const nonceValue = match[1]; + if (nonceValue.length < 8) { + violations.push( + new Finding( + Type.NONCE_LENGTH, + 'Nonces should be at least 8 characters long.', + Severity.MEDIUM, + directive, + value + ) + ); + } + + if (!isNonce(value, true)) { + violations.push( + new Finding( + Type.NONCE_CHARSET, + 'Nonces should only use the base64 charset.', + Severity.INFO, + directive, + value + ) + ); + } + } + } + ); + } + + return violations; +} + +/** + * Checks if CSP allows sourcing from http:// + * Findings of this check are of medium severity and are FP free. + * + * Example policy where this check would trigger: + * report-uri http://foo.bar/csp + * + * @param parsedCsp Parsed CSP. + */ +export function checkSrcHttp(parsedCsps: Csp): Finding[] { + const violations: Finding[] = []; + + const directivesForcingHttps: string[] = []; + const directivesWithViolation: Record = {}; + + for (const cspChecked of parsedCsps.directives) { + utils.applyCheckFunktionToDirectives( + cspChecked, + (directive, directiveValues) => { + for (const value of directiveValues) { + if (value === 'https:') { + directivesForcingHttps.push(directive); + } else if (value.startsWith('http://')) { + // Check if reporting violations via http:// is allowed + if (directive === Directive.REPORT_URI) { + violations.push( + new Finding( + Type.SRC_HTTP, + 'Use HTTPS to send violation reports securely.', + Severity.MEDIUM, + Directive.REPORT_URI, + value + ) + ); + } else { + directivesWithViolation[directive] = directivesWithViolation[ + directive + ] + ? directivesWithViolation[directive] + : []; + directivesWithViolation[directive].push(directive); + } + } + } + } + ); + } + + for (const directive in directivesWithViolation) { + for (const value in directivesWithViolation[directive]) { + if (directive in directivesForcingHttps) { + violations.push( + new Finding( + Type.CONFLICTING_DIRECTIVES, + 'Resources are only allowed over HTTPS but HTTP hosts are specified.', + Severity.LOW, + directive, + value + ) + ); + } else { + violations.push( + new Finding( + Type.SRC_HTTP, + 'Allow only resources downloaded over HTTPS.', + Severity.MEDIUM, + directive, + value + ) + ); + } + } + } + + return violations; +} + +/** + * Checks if the policy has configured reporting in a robust manner. + */ +export function checkHasConfiguredReporting(parsedCsps: Csp): Finding[] { + const findings: Finding[] = []; + + for (const cspChecked of parsedCsps.directives) { + const reportUriValues: string[] = cspChecked[Directive.REPORT_URI] || []; + if (reportUriValues.length > 0) { + continue; + } + + const reportToValues: string[] = cspChecked[Directive.REPORT_TO] || []; + if (reportToValues.length > 0) { + findings.push( + new Finding( + Type.REPORT_TO_ONLY, + "This CSP policy only provides a reporting destination via the 'report-to' directive. This directive is only supported in Chromium-based browsers so it is recommended to also use a 'report-uri' directive.", + Severity.INFO, + Directive.REPORT_TO + ) + ); + } else { + findings.push( + new Finding( + Type.REPORTING_DESTINATION_MISSING, + 'This CSP policy does not configure a reporting destination. This makes it difficult to maintain the CSP policy over time and monitor for any breakages.', + Severity.INFO, + Directive.REPORT_URI + ) + ); + } + } + + return findings; +} diff --git a/src/checks/strictcsp_checks.ts b/src/checks/strictcsp_checks.ts new file mode 100644 index 0000000..bd7ffcc --- /dev/null +++ b/src/checks/strictcsp_checks.ts @@ -0,0 +1,209 @@ +/** + * @fileoverview Collection of "strict" CSP and backward compatibility checks. + * A "strict" CSP is based on nonces or hashes and drops the allowlist. + * These checks ensure that 'strict-dynamic' and a CSP nonce/hash are present. + * Due to 'strict-dynamic' any allowlist will get dropped in CSP3. + * The backward compatibility checks ensure that the strict nonce/hash based CSP + * will be a no-op in older browsers by checking for presence of 'unsafe-inline' + * (will be dropped in newer browsers if a nonce or hash is present) and for + * prsensence of http: and https: url schemes (will be droped in the presence of + * 'strict-dynamic' in newer browsers). + * + * @author lwe@google.com (Lukas Weichselbaum) + * + * @license + * Copyright 2016 Google Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Csp, Directive, Keyword, TrustedTypesSink} from '../csp'; +import {Finding, Severity, Type} from '../finding'; + +/** + * Checks if 'strict-dynamic' is present. + * + * Example policy where this check would trigger: + * script-src foo.bar + * + * @param parsedCsp A parsed csp. + */ +export function checkStrictDynamic(parsedCsps: Csp): Finding[] { + const directiveName = parsedCsps.getEffectiveDirective(Directive.SCRIPT_SRC); + const findings: Finding[] = []; + + for (const currentCsp of parsedCsps.directives) { + const values: string[] = currentCsp[directiveName] || []; + + const schemeOrHostPresent = values.some(v => !v.startsWith("'")); + + // Check if strict-dynamic is present in case a host/scheme allowlist is used. + if (schemeOrHostPresent && !values.includes(Keyword.STRICT_DYNAMIC)) { + findings.push( + new Finding( + Type.STRICT_DYNAMIC, + 'Host allowlists can frequently be bypassed. Consider using ' + + "'strict-dynamic' in combination with CSP nonces or hashes.", + Severity.STRICT_CSP, + directiveName + ) + ); + } + } + + return findings; +} + +/** + * Checks if 'strict-dynamic' is only used together with a nonce or a hash. + * + * Example policy where this check would trigger: + * script-src 'strict-dynamic' + * + * @param parsedCsp A parsed csp. + */ +export function checkStrictDynamicNotStandalone(parsedCsps: Csp): Finding[] { + const directiveName = parsedCsps.getEffectiveDirective(Directive.SCRIPT_SRC); + + if ( + parsedCsps.policyHasStrictDynamic() && + !parsedCsps.policyHasScriptNonces() && + !parsedCsps.policyHasScriptHashes() + ) { + return [ + new Finding( + Type.STRICT_DYNAMIC_NOT_STANDALONE, + "'strict-dynamic' without a CSP nonce/hash will block all scripts.", + Severity.INFO, + directiveName + ), + ]; + } + + return []; +} + +/** + * Checks if the policy has 'unsafe-inline' when a nonce or hash are present. + * This will ensure backward compatibility to browser that don't support + * CSP nonces or hasehs. + * + * Example policy where this check would trigger: + * script-src 'nonce-test' + * + * @param parsedCsp A parsed csp. + */ +export function checkUnsafeInlineFallback(parsedCsps: Csp): Finding[] { + if ( + !parsedCsps.policyHasScriptNonces() && + !parsedCsps.policyHasScriptHashes() + ) { + return []; + } + + const directiveName = parsedCsps.getEffectiveDirective(Directive.SCRIPT_SRC); + const findings: Finding[] = []; + + for (const currentCsp of parsedCsps.directives) { + const values: string[] = currentCsp[directiveName] || []; + + if (!values.includes(Keyword.UNSAFE_INLINE)) { + findings.push( + new Finding( + Type.UNSAFE_INLINE_FALLBACK, + "Consider adding 'unsafe-inline' (ignored by browsers supporting " + + 'nonces/hashes) to be backward compatible with older browsers.', + Severity.STRICT_CSP, + directiveName + ) + ); + } + } + + return findings; +} + +/** + * Checks if the policy has an allowlist fallback (* or http: and https:) when + * 'strict-dynamic' is present. + * This will ensure backward compatibility to browser that don't support + * 'strict-dynamic'. + * + * Example policy where this check would trigger: + * script-src 'nonce-test' 'strict-dynamic' + * + * @param parsedCsp A parsed csp. + */ +export function checkAllowlistFallback(parsedCsps: Csp): Finding[] { + const directiveName = parsedCsps.getEffectiveDirective(Directive.SCRIPT_SRC); + const findings: Finding[] = []; + + for (const currentCsp of parsedCsps.directives) { + const values: string[] = currentCsp[directiveName] || []; + + if (!values.includes(Keyword.STRICT_DYNAMIC)) { + return []; + } + + // Check if there's already an allowlist (url scheme or url) + if ( + !values.some(v => ['http:', 'https:', '*'].includes(v) || v.includes('.')) + ) { + findings.push( + new Finding( + Type.ALLOWLIST_FALLBACK, + 'Consider adding https: and http: url schemes (ignored by browsers ' + + "supporting 'strict-dynamic') to be backward compatible with older " + + 'browsers.', + Severity.STRICT_CSP, + directiveName + ) + ); + } + } + + return findings; +} + +/** + * Checks if the policy requires Trusted Types for scripts. + * + * I.e. the policy should have the following dirctive: + * require-trusted-types-for 'script' + * + * @param parsedCsp A parsed csp. + */ +export function checkRequiresTrustedTypesForScripts( + parsedCsps: Csp +): Finding[] { + const directiveName = parsedCsps.getEffectiveDirective( + Directive.REQUIRE_TRUSTED_TYPES_FOR + ); + for (const cspChecked of parsedCsps.directives) { + const values: string[] = cspChecked[directiveName] || []; + + if (values.includes(TrustedTypesSink.SCRIPT)) { + return []; + } + } + + return [ + new Finding( + Type.REQUIRE_TRUSTED_TYPES_FOR_SCRIPTS, + 'Consider requiring Trusted Types for scripts to lock down DOM XSS ' + + 'injection sinks. You can do this by adding ' + + '"require-trusted-types-for \'script\'" to your policy.', + Severity.INFO, + Directive.REQUIRE_TRUSTED_TYPES_FOR + ), + ]; +} diff --git a/csp.ts b/src/csp.ts similarity index 56% rename from csp.ts rename to src/csp.ts index 0e1daf6..ee9b573 100644 --- a/csp.ts +++ b/src/csp.ts @@ -17,7 +17,6 @@ * limitations under the License. */ - import {Finding, Severity, Type} from './finding'; /** @@ -27,7 +26,7 @@ import {Finding, Severity, Type} from './finding'; * - https://www.w3.org/TR/upgrade-insecure-requests/ */ export class Csp { - directives: Record = {}; + directives: Record[] = []; /** * Clones a CSP object. @@ -35,12 +34,20 @@ export class Csp { */ clone(): Csp { const clone = new Csp(); - for (const [directive, directiveValues] of Object.entries( - this.directives)) { - if (directiveValues) { - clone.directives[directive] = [...directiveValues]; + for (const currentDirective of this.directives) { + const directiveClone: Record = {}; + + for (const [directiveName, directiveValues] of Object.entries( + currentDirective + )) { + if (directiveValues) { + directiveClone[directiveName] = [...directiveValues]; + } } + + clone.directives.push(directiveClone); } + return clone; } @@ -49,21 +56,27 @@ export class Csp { * @return CSP string. */ convertToString(): string { - let cspString = ''; - - for (const [directive, directiveValues] of Object.entries( - this.directives)) { - cspString += directive; - if (directiveValues !== undefined) { - for (let value, i = 0; (value = directiveValues[i]); i++) { - cspString += ' '; - cspString += value; + const cspStrings: string[] = []; + + for (const currentDirective of this.directives) { + let cspString: string = ''; + for (const [directive, directiveValues] of Object.entries( + currentDirective + )) { + cspString += directive; + if (directiveValues !== undefined) { + for (let value, i = 0; (value = directiveValues[i]); i++) { + cspString += ' '; + cspString += value; + } } + cspString += '; '; } - cspString += '; '; + + cspStrings.push(cspString.trim()); } - return cspString; + return cspStrings.join(', '); } /** @@ -78,63 +91,83 @@ export class Csp { const findings = optFindings || []; const effectiveCsp = this.clone(); const directive = effectiveCsp.getEffectiveDirective(Directive.SCRIPT_SRC); - const values = this.directives[directive] || []; - const effectiveCspValues = effectiveCsp.directives[directive]; - if (effectiveCspValues && + for (let index = 0; index < effectiveCsp.directives.length; index++) { + const values = this.directives[index][directive] || []; + const effectiveCspValues = effectiveCsp.directives[index][directive]; + + if ( + effectiveCspValues && (effectiveCsp.policyHasScriptNonces() || - effectiveCsp.policyHasScriptHashes())) { - if (cspVersion >= Version.CSP2) { - // Ignore 'unsafe-inline' in CSP >= v2, if a nonce or a hash is present. - if (values.includes(Keyword.UNSAFE_INLINE)) { - arrayRemove(effectiveCspValues, Keyword.UNSAFE_INLINE); - findings.push(new Finding( - Type.IGNORED, - 'unsafe-inline is ignored if a nonce or a hash is present. ' + + effectiveCsp.policyHasScriptHashes()) + ) { + if (cspVersion >= Version.CSP2) { + // Ignore 'unsafe-inline' in CSP >= v2, if a nonce or a hash is present. + if (values.includes(Keyword.UNSAFE_INLINE)) { + arrayRemove(effectiveCspValues, Keyword.UNSAFE_INLINE); + findings.push( + new Finding( + Type.IGNORED, + 'unsafe-inline is ignored if a nonce or a hash is present. ' + '(CSP2 and above)', - Severity.NONE, directive, Keyword.UNSAFE_INLINE)); - } - } else { - // remove nonces and hashes (not supported in CSP < v2). - for (const value of values) { - if (value.startsWith('\'nonce-') || value.startsWith('\'sha')) { - arrayRemove(effectiveCspValues, value); + Severity.NONE, + directive, + Keyword.UNSAFE_INLINE + ) + ); + } + } else { + // remove nonces and hashes (not supported in CSP < v2). + for (const value of values) { + if (value.startsWith("'nonce-") || value.startsWith("'sha")) { + arrayRemove(effectiveCspValues, value); + } } } } - } - if (effectiveCspValues && this.policyHasStrictDynamic()) { - // Ignore allowlist in CSP >= v3 in presence of 'strict-dynamic'. - if (cspVersion >= Version.CSP3) { - for (const value of values) { - // Because of 'strict-dynamic' all host-source and scheme-source - // expressions, as well as the "'unsafe-inline'" and "'self' - // keyword-sources will be ignored. - // https://w3c.github.io/webappsec-csp/#strict-dynamic-usage - if (!value.startsWith('\'') || value === Keyword.SELF || - value === Keyword.UNSAFE_INLINE) { - arrayRemove(effectiveCspValues, value); - findings.push(new Finding( - Type.IGNORED, - 'Because of strict-dynamic this entry is ignored in CSP3 and above', - Severity.NONE, directive, value)); + if (effectiveCspValues && this.policyHasStrictDynamic()) { + // Ignore allowlist in CSP >= v3 in presence of 'strict-dynamic'. + if (cspVersion >= Version.CSP3) { + for (const value of values) { + // Because of 'strict-dynamic' all host-source and scheme-source + // expressions, as well as the "'unsafe-inline'" and "'self' + // keyword-sources will be ignored. + // https://w3c.github.io/webappsec-csp/#strict-dynamic-usage + if ( + !value.startsWith("'") || + value === Keyword.SELF || + value === Keyword.UNSAFE_INLINE + ) { + arrayRemove(effectiveCspValues, value); + findings.push( + new Finding( + Type.IGNORED, + 'Because of strict-dynamic this entry is ignored in CSP3 and above', + Severity.NONE, + directive, + value + ) + ); + } } + } else { + // strict-dynamic not supported. + arrayRemove(effectiveCspValues, Keyword.STRICT_DYNAMIC); } - } else { - // strict-dynamic not supported. - arrayRemove(effectiveCspValues, Keyword.STRICT_DYNAMIC); } - } - if (cspVersion < Version.CSP3) { - // Remove CSP3 directives from pre-CSP3 policies. - // https://w3c.github.io/webappsec-csp/#changes-from-level-2 - delete effectiveCsp.directives[Directive.REPORT_TO]; - delete effectiveCsp.directives[Directive.WORKER_SRC]; - delete effectiveCsp.directives[Directive.MANIFEST_SRC]; - delete effectiveCsp.directives[Directive.TRUSTED_TYPES]; - delete effectiveCsp.directives[Directive.REQUIRE_TRUSTED_TYPES_FOR]; + if (cspVersion < Version.CSP3) { + // Remove CSP3 directives from pre-CSP3 policies. + // https://w3c.github.io/webappsec-csp/#changes-from-level-2 + delete effectiveCsp.directives[index][Directive.REPORT_TO]; + delete effectiveCsp.directives[index][Directive.WORKER_SRC]; + delete effectiveCsp.directives[index][Directive.MANIFEST_SRC]; + delete effectiveCsp.directives[index][Directive.TRUSTED_TYPES]; + delete effectiveCsp.directives[index][ + Directive.REQUIRE_TRUSTED_TYPES_FOR + ]; + } } return effectiveCsp; @@ -147,13 +180,19 @@ export class Csp { * @return The effective directive. */ getEffectiveDirective(directive: string): string { - // Only fetch directives default to default-src. - if (!(directive in this.directives) && - FETCH_DIRECTIVES.includes(directive as Directive)) { - return Directive.DEFAULT_SRC; + // Directive doesn't default to default-src + if (!FETCH_DIRECTIVES.includes(directive as Directive)) { + return directive; + } + + // Look in each CSP to find the directive + for (const currentCsp of this.directives) { + if (directive in currentCsp) { + return directive; + } } - return directive; + return Directive.DEFAULT_SRC; } /** @@ -163,8 +202,9 @@ export class Csp { * @return The effective directives. */ getEffectiveDirectives(directives: string[]): string[] { - const effectiveDirectives = - new Set(directives.map((val) => this.getEffectiveDirective(val))); + const effectiveDirectives = new Set( + directives.map(val => this.getEffectiveDirective(val)) + ); return [...effectiveDirectives]; } @@ -174,8 +214,15 @@ export class Csp { */ policyHasScriptNonces(): boolean { const directiveName = this.getEffectiveDirective(Directive.SCRIPT_SRC); - const values = this.directives[directiveName] || []; - return values.some((val) => isNonce(val)); + + for (const currentDirective of this.directives) { + const values = currentDirective[directiveName] || []; + if (values.some(val => isNonce(val))) { + return true; + } + } + + return false; } /** @@ -184,8 +231,15 @@ export class Csp { */ policyHasScriptHashes(): boolean { const directiveName = this.getEffectiveDirective(Directive.SCRIPT_SRC); - const values = this.directives[directiveName] || []; - return values.some((val) => isHash(val)); + + for (const currentDirective of this.directives) { + const values = currentDirective[directiveName] || []; + if (values.some(val => isHash(val))) { + return true; + } + } + + return false; } /** @@ -194,39 +248,43 @@ export class Csp { */ policyHasStrictDynamic(): boolean { const directiveName = this.getEffectiveDirective(Directive.SCRIPT_SRC); - const values = this.directives[directiveName] || []; - return values.includes(Keyword.STRICT_DYNAMIC); + + for (const currentDirective of this.directives) { + const values = currentDirective[directiveName] || []; + if (values.includes(Keyword.STRICT_DYNAMIC)) { + return true; + } + } + + return false; } } - /** * CSP directive source keywords. */ export enum Keyword { - SELF = '\'self\'', - NONE = '\'none\'', - UNSAFE_INLINE = '\'unsafe-inline\'', - UNSAFE_EVAL = '\'unsafe-eval\'', - WASM_EVAL = '\'wasm-eval\'', - WASM_UNSAFE_EVAL = '\'wasm-unsafe-eval\'', - STRICT_DYNAMIC = '\'strict-dynamic\'', - UNSAFE_HASHED_ATTRIBUTES = '\'unsafe-hashed-attributes\'', - UNSAFE_HASHES = '\'unsafe-hashes\'', - REPORT_SAMPLE = '\'report-sample\'', - BLOCK = '\'block\'', - ALLOW = '\'allow\'', + SELF = "'self'", + NONE = "'none'", + UNSAFE_INLINE = "'unsafe-inline'", + UNSAFE_EVAL = "'unsafe-eval'", + WASM_EVAL = "'wasm-eval'", + WASM_UNSAFE_EVAL = "'wasm-unsafe-eval'", + STRICT_DYNAMIC = "'strict-dynamic'", + UNSAFE_HASHED_ATTRIBUTES = "'unsafe-hashed-attributes'", + UNSAFE_HASHES = "'unsafe-hashes'", + REPORT_SAMPLE = "'report-sample'", + BLOCK = "'block'", + ALLOW = "'allow'", } - /** * CSP directive source keywords. */ export enum TrustedTypesSink { - SCRIPT = '\'script\'', + SCRIPT = "'script'", } - /** * CSP v3 directives. * List of valid CSP directives: @@ -289,12 +347,22 @@ export enum Directive { * */ export const FETCH_DIRECTIVES: Directive[] = [ - Directive.CHILD_SRC, Directive.CONNECT_SRC, Directive.DEFAULT_SRC, - Directive.FONT_SRC, Directive.FRAME_SRC, Directive.IMG_SRC, - Directive.MANIFEST_SRC, Directive.MEDIA_SRC, Directive.OBJECT_SRC, - Directive.SCRIPT_SRC, Directive.SCRIPT_SRC_ATTR, Directive.SCRIPT_SRC_ELEM, - Directive.STYLE_SRC, Directive.STYLE_SRC_ATTR, Directive.STYLE_SRC_ELEM, - Directive.WORKER_SRC + Directive.CHILD_SRC, + Directive.CONNECT_SRC, + Directive.DEFAULT_SRC, + Directive.FONT_SRC, + Directive.FRAME_SRC, + Directive.IMG_SRC, + Directive.MANIFEST_SRC, + Directive.MEDIA_SRC, + Directive.OBJECT_SRC, + Directive.SCRIPT_SRC, + Directive.SCRIPT_SRC_ATTR, + Directive.SCRIPT_SRC_ELEM, + Directive.STYLE_SRC, + Directive.STYLE_SRC_ATTR, + Directive.STYLE_SRC_ELEM, + Directive.WORKER_SRC, ]; /** @@ -303,10 +371,9 @@ export const FETCH_DIRECTIVES: Directive[] = [ export enum Version { CSP1 = 1, CSP2, - CSP3 + CSP3, } - /** * Checks if a string is a valid CSP directive. * @param directive value to check. @@ -316,7 +383,6 @@ export function isDirective(directive: string): boolean { return Object.values(Directive).includes(directive as Directive); } - /** * Checks if a string is a valid CSP keyword. * @param keyword value to check. @@ -326,7 +392,6 @@ export function isKeyword(keyword: string): boolean { return Object.values(Keyword).includes(keyword as Keyword); } - /** * Checks if a string is a valid URL scheme. * Scheme part + ":" @@ -339,17 +404,15 @@ export function isUrlScheme(urlScheme: string): boolean { return pattern.test(urlScheme); } - /** * A regex pattern to check nonce prefix and Base64 formatting of a nonce value. */ -export const STRICT_NONCE_PATTERN = - new RegExp('^\'nonce-[a-zA-Z0-9+/_-]+[=]{0,2}\'$'); - +export const STRICT_NONCE_PATTERN = new RegExp( + "^'nonce-[a-zA-Z0-9+/_-]+[=]{0,2}'$" +); /** A regex pattern for checking if nonce prefix. */ -export const NONCE_PATTERN = new RegExp('^\'nonce-(.+)\'$'); - +export const NONCE_PATTERN = new RegExp("^'nonce-(.+)'$"); /** * Checks if a string is a valid CSP nonce. @@ -363,17 +426,15 @@ export function isNonce(nonce: string, strictCheck?: boolean): boolean { return pattern.test(nonce); } - /** * A regex pattern to check hash prefix and Base64 formatting of a hash value. */ -export const STRICT_HASH_PATTERN = - new RegExp('^\'(sha256|sha384|sha512)-[a-zA-Z0-9+/]+[=]{0,2}\'$'); - +export const STRICT_HASH_PATTERN = new RegExp( + "^'(sha256|sha384|sha512)-[a-zA-Z0-9+/]+[=]{0,2}'$" +); /** A regex pattern to check hash prefix. */ -export const HASH_PATTERN = new RegExp('^\'(sha256|sha384|sha512)-(.+)\'$'); - +export const HASH_PATTERN = new RegExp("^'(sha256|sha384|sha512)-(.+)'$"); /** * Checks if a string is a valid CSP hash. @@ -387,7 +448,6 @@ export function isHash(hash: string, strictCheck?: boolean): boolean { return pattern.test(hash); } - /** * Class to represent all generic CSP errors. */ diff --git a/evaluator.ts b/src/evaluator.ts similarity index 74% rename from evaluator.ts rename to src/evaluator.ts index 7a44eab..e2eb038 100644 --- a/evaluator.ts +++ b/src/evaluator.ts @@ -20,12 +20,9 @@ import {CheckerFunction} from './checks/checker'; import * as parserChecks from './checks/parser_checks'; import * as securityChecks from './checks/security_checks'; import * as strictcspChecks from './checks/strictcsp_checks'; -import * as csp from './csp'; import {Csp, Version} from './csp'; import {Finding} from './finding'; - - /** * A class to hold a CSP Evaluator. * Evaluates a parsed CSP and reports security findings. @@ -41,19 +38,19 @@ export class CspEvaluator { */ findings: Finding[] = []; /** - * @param parsedCsp A parsed Content Security Policy. + * @param parsedCsp A parsed list of Content Security Policy. * @param cspVersion CSP version to apply checks for. */ - constructor(parsedCsp: Csp, cspVersion?: Version) { + constructor(parsedCsps: Csp, cspVersion?: Version) { /** * CSP version. */ - this.version = cspVersion || csp.Version.CSP3; + this.version = cspVersion || Version.CSP3; /** - * Parsed CSP. + * Parsed CSPs. */ - this.csp = parsedCsp; + this.csp = parsedCsps; } /** @@ -66,8 +63,9 @@ export class CspEvaluator { * @export */ evaluate( - parsedCspChecks?: CheckerFunction[], - effectiveCspChecks?: CheckerFunction[]): Finding[] { + parsedCspChecks?: CheckerFunction[], + effectiveCspChecks?: CheckerFunction[] + ): Finding[] { this.findings = []; const checks = effectiveCspChecks || DEFAULT_CHECKS; @@ -75,7 +73,7 @@ export class CspEvaluator { // supporting a specific version of CSP. // For example a browser supporting only CSP1 will ignore nonces and // therefore 'unsafe-inline' would not get ignored if a policy has nonces. - const effectiveCsp = this.csp.getEffectiveCsp(this.version, this.findings); + const effectiveCsps = this.csp.getEffectiveCsp(this.version, this.findings); // Checks independent of CSP version. if (parsedCspChecks) { @@ -86,29 +84,33 @@ export class CspEvaluator { // Checks depenent on CSP version. for (const check of checks) { - this.findings = this.findings.concat(check(effectiveCsp)); + this.findings = this.findings.concat(check(effectiveCsps)); } return this.findings; } } - /** * Set of default checks to run. */ export const DEFAULT_CHECKS: CheckerFunction[] = [ - securityChecks.checkScriptUnsafeInline, securityChecks.checkScriptUnsafeEval, - securityChecks.checkPlainUrlSchemes, securityChecks.checkWildcards, + securityChecks.checkScriptUnsafeInline, + securityChecks.checkScriptUnsafeEval, + securityChecks.checkPlainUrlSchemes, + securityChecks.checkWildcards, securityChecks.checkMissingDirectives, securityChecks.checkScriptAllowlistBypass, - securityChecks.checkFlashObjectAllowlistBypass, securityChecks.checkIpSource, - securityChecks.checkNonceLength, securityChecks.checkSrcHttp, - securityChecks.checkDeprecatedDirective, parserChecks.checkUnknownDirective, - parserChecks.checkMissingSemicolon, parserChecks.checkInvalidKeyword + securityChecks.checkFlashObjectAllowlistBypass, + securityChecks.checkIpSource, + securityChecks.checkNonceLength, + securityChecks.checkSrcHttp, + securityChecks.checkDeprecatedDirective, + parserChecks.checkUnknownDirective, + parserChecks.checkMissingSemicolon, + parserChecks.checkInvalidKeyword, ]; - /** * Strict CSP and backward compatibility checks. */ @@ -117,5 +119,5 @@ export const STRICTCSP_CHECKS: CheckerFunction[] = [ strictcspChecks.checkStrictDynamicNotStandalone, strictcspChecks.checkUnsafeInlineFallback, strictcspChecks.checkAllowlistFallback, - strictcspChecks.checkRequiresTrustedTypesForScripts + strictcspChecks.checkRequiresTrustedTypesForScripts, ]; diff --git a/finding.ts b/src/finding.ts similarity index 79% rename from finding.ts rename to src/finding.ts index e502a49..e09f471 100644 --- a/finding.ts +++ b/src/finding.ts @@ -16,7 +16,6 @@ * @author lwe@google.com (Lukas Weichselbaum) */ - /** * A CSP Finding is returned by a CSP check and can either reference a directive * value or a directive. If a directive value is referenced opt_index must be @@ -32,8 +31,12 @@ export class Finding { * @param value The directive value, if exists. */ constructor( - public type: Type, public description: string, public severity: Severity, - public directive: string, public value?: string) {} + public type: Type, + public description: string, + public severity: Severity, + public directive: string, + public value?: string + ) {} /** * Returns the highest severity of a list of findings. @@ -45,8 +48,8 @@ export class Finding { return Severity.NONE; } - const severities = findings.map((finding) => finding.severity); - const min = (prev: Severity, cur: Severity) => prev < cur ? prev : cur; + const severities = findings.map(finding => finding.severity); + const min = (prev: Severity, cur: Severity) => (prev < cur ? prev : cur); return severities.reduce(min, Severity.NONE); } @@ -54,13 +57,16 @@ export class Finding { if (!(obj instanceof Finding)) { return false; } - return obj.type === this.type && obj.description === this.description && - obj.severity === this.severity && obj.directive === this.directive && - obj.value === this.value; + return ( + obj.type === this.type && + obj.description === this.description && + obj.severity === this.severity && + obj.directive === this.directive && + obj.value === this.value + ); } } - /** * Finding severities. */ @@ -71,11 +77,12 @@ export enum Severity { HIGH_MAYBE = 40, STRICT_CSP = 45, MEDIUM_MAYBE = 50, + LOW = 70, + LOW_MAYBE = 80, INFO = 60, - NONE = 100 + NONE = 100, } - /** * Finding types for evluator checks. */ @@ -90,12 +97,16 @@ export enum Type { MISSING_DIRECTIVES = 300, SCRIPT_UNSAFE_INLINE, SCRIPT_UNSAFE_EVAL, + STYLE_UNSAFE_INLINE, + STYLE_UNSAFE_EVAL, + SCRIPT_WASM_UNSAFE_EVAL, PLAIN_URL_SCHEMES, PLAIN_WILDCARD, SCRIPT_ALLOWLIST_BYPASS, OBJECT_ALLOWLIST_BYPASS, NONCE_LENGTH, IP_SOURCE, + CONFLICTING_DIRECTIVES, DEPRECATED_DIRECTIVE, SRC_HTTP, diff --git a/lighthouse/lighthouse_checks.ts b/src/lighthouse/lighthouse_checks.ts similarity index 73% rename from lighthouse/lighthouse_checks.ts rename to src/lighthouse/lighthouse_checks.ts index 58cd97a..5ce04fe 100644 --- a/lighthouse/lighthouse_checks.ts +++ b/src/lighthouse/lighthouse_checks.ts @@ -4,11 +4,29 @@ */ import {CheckerFunction} from '../checks/checker'; -import {checkInvalidKeyword, checkMissingSemicolon, checkUnknownDirective} from '../checks/parser_checks'; -import {checkDeprecatedDirective, checkMissingObjectSrcDirective, checkMissingScriptSrcDirective, checkMultipleMissingBaseUriDirective, checkNonceLength, checkPlainUrlSchemes, checkScriptUnsafeInline, checkWildcards} from '../checks/security_checks'; -import {checkAllowlistFallback, checkStrictDynamic, checkUnsafeInlineFallback} from '../checks/strictcsp_checks'; +import { + checkInvalidKeyword, + checkMissingSemicolon, + checkUnknownDirective, +} from '../checks/parser_checks'; +import { + checkDeprecatedDirective, + checkMissingBaseUriDirective, + checkMissingObjectSrcDirective, + checkMissingScriptSrcDirective, + checkNonceLength, + checkPlainUrlSchemes, + checkScriptUnsafeInline, + checkWildcards, +} from '../checks/security_checks'; +import { + checkAllowlistFallback, + checkStrictDynamic, + checkUnsafeInlineFallback, +} from '../checks/strictcsp_checks'; import {Csp, Directive, Version} from '../csp'; import {Finding} from '../finding'; +import {CspParser} from '../parser'; interface Equalable { equals(a: unknown): boolean; @@ -58,7 +76,9 @@ function setUnion(sets: T[][]): T[] { * first one that had any findings. */ function atLeastOnePasses( - parsedCsps: Csp[], checker: CheckerFunction): Finding[] { + parsedCsps: Csp[], + checker: CheckerFunction +): Finding[] { const findings: Finding[][] = []; for (const parsedCsp of parsedCsps) { findings.push(checker(parsedCsp)); @@ -71,7 +91,9 @@ function atLeastOnePasses( * list of findings from the one that had the most findings. */ function atLeastOneFails( - parsedCsps: Csp[], checker: CheckerFunction): Finding[] { + parsedCsps: Csp[], + checker: CheckerFunction +): Finding[] { const findings: Finding[][] = []; for (const parsedCsp of parsedCsps) { findings.push(checker(parsedCsp)); @@ -84,19 +106,29 @@ function atLeastOneFails( * mark the CSP as failing. Returns only the first set of failures. */ export function evaluateForFailure(parsedCsps: Csp[]): Finding[] { + const mergedCspString: string[] = []; + for (const csp of parsedCsps) { + mergedCspString.push(csp.convertToString()); + } + const mergedCsp: Csp = new CspParser(mergedCspString).csp; + // Check #1 const targetsXssFindings = [ ...atLeastOnePasses(parsedCsps, checkMissingScriptSrcDirective), ...atLeastOnePasses(parsedCsps, checkMissingObjectSrcDirective), - ...checkMultipleMissingBaseUriDirective(parsedCsps), + ...checkMissingBaseUriDirective(mergedCsp), ]; // Check #2 - const effectiveCsps = - parsedCsps.map(csp => csp.getEffectiveCsp(Version.CSP3)); + const effectiveCsps = parsedCsps.map(csp => + csp.getEffectiveCsp(Version.CSP3) + ); const effectiveCspsWithScript = effectiveCsps.filter(csp => { const directiveName = csp.getEffectiveDirective(Directive.SCRIPT_SRC); - return csp.directives[directiveName]; + for (const currentCsp of csp.directives) { + return currentCsp[directiveName]; + } + return false; }); const robust = [ ...atLeastOnePasses(effectiveCspsWithScript, checkStrictDynamic), @@ -119,7 +151,7 @@ export function evaluateForWarnings(parsedCsps: Csp[]): Finding[] { // Check #3 return [ ...atLeastOneFails(parsedCsps, checkUnsafeInlineFallback), - ...atLeastOneFails(parsedCsps, checkAllowlistFallback) + ...atLeastOneFails(parsedCsps, checkAllowlistFallback), ]; } @@ -133,9 +165,11 @@ export function evaluateForSyntaxErrors(parsedCsps: Csp[]): Finding[][] { const allFindings: Finding[][] = []; for (const csp of parsedCsps) { const findings = [ - ...checkNonceLength(csp), ...checkUnknownDirective(csp), - ...checkDeprecatedDirective(csp), ...checkMissingSemicolon(csp), - ...checkInvalidKeyword(csp) + ...checkNonceLength(csp), + ...checkUnknownDirective(csp), + ...checkDeprecatedDirective(csp), + ...checkMissingSemicolon(csp), + ...checkInvalidKeyword(csp), ]; allFindings.push(findings); } diff --git a/parser.ts b/src/parser.ts similarity index 51% rename from parser.ts rename to src/parser.ts index 584f954..7ab9f8f 100644 --- a/parser.ts +++ b/src/parser.ts @@ -16,69 +16,92 @@ * @author lwe@google.com (Lukas Weichselbaum) */ -import * as csp from './csp'; - - +import {Csp, isKeyword, isUrlScheme} from './csp'; +import {mergeCspHeaders} from './utils'; /** * A class to hold a parser for CSP in string format. * @unrestricted */ export class CspParser { - csp: csp.Csp; + csp: Csp; + /** * @param unparsedCsp A Content Security Policy as string. */ - constructor(unparsedCsp: string) { + constructor(unparsedCsps: string | string[]) { /** * Parsed CSP */ - this.csp = new csp.Csp(); + this.csp = new Csp(); + + if (Array.isArray(unparsedCsps)) { + unparsedCsps = mergeCspHeaders(unparsedCsps); + } - this.parse(unparsedCsp); + this.parse(unparsedCsps); } /** * Parses a CSP from a string. * @param unparsedCsp CSP as string. */ - parse(unparsedCsp: string): csp.Csp { - // Reset the internal state: - this.csp = new csp.Csp(); + parse(unparsedCsp: string): Csp { + unparsedCsp.split(', ').forEach(currentCsp => { + this.csp.directives.push(this.parseCsp(currentCsp)); + }); + + return this.csp; + } - // Split CSP into directive tokens. + parseCsp(unparsedCsp: string): Record { + const retCspDirectives: Record = {}; + + // For each token returned by strictly splitting serialized on the U+003B SEMICOLON character (;): const directiveTokens = unparsedCsp.split(';'); for (let i = 0; i < directiveTokens.length; i++) { + // Strip leading and trailing ASCII whitespace from token. const directiveToken = directiveTokens[i].trim(); - // Split directive tokens into directive name and directive values. + // If token is an empty string, or if token is not an ASCII string, continue. + /* eslint-disable no-control-regex */ + if (directiveToken === '' || !/^[\x00-\xFF]*$/.test(directiveToken)) { + continue; + } + /* eslint-enable no-control-regex */ + + // Let directive name be the result of collecting a sequence of code points from token which are not ASCII whitespace. + // Let directive value be the result of splitting token on ASCII whitespace. const directiveParts = directiveToken.match(/\S+/g); if (Array.isArray(directiveParts)) { + // Set directive name to be the result of running ASCII lowercase on directive name. const directiveName = directiveParts[0].toLowerCase(); - // If the set of directives already contains a directive whose name is a - // case insensitive match for directive name, ignore this instance of - // the directive and continue to the next token. - if (directiveName in this.csp.directives) { + // If policy’s directive set contains a directive whose name is directive name, continue. + if (directiveName in retCspDirectives) { continue; } - if (!csp.isDirective(directiveName)) { - } - const directiveValues: string[] = []; - for (let directiveValue, j = 1; (directiveValue = directiveParts[j]); - j++) { + for ( + let directiveValue, j = 1; + (directiveValue = directiveParts[j]); + j++ + ) { + // Let directive be a new directive whose name is directive name, and value is directive value. directiveValue = normalizeDirectiveValue(directiveValue); if (!directiveValues.includes(directiveValue)) { directiveValues.push(directiveValue); } } - this.csp.directives[directiveName] = directiveValues; + + // Append directive to policy’s directive set. + retCspDirectives[directiveName] = directiveValues; } } - return this.csp; + // Return policy. + return retCspDirectives; } } @@ -91,7 +114,7 @@ export class CspParser { function normalizeDirectiveValue(directiveValue: string): string { directiveValue = directiveValue.trim(); const directiveValueLower = directiveValue.toLowerCase(); - if (csp.isKeyword(directiveValueLower) || csp.isUrlScheme(directiveValue)) { + if (isKeyword(directiveValueLower) || isUrlScheme(directiveValue)) { return directiveValueLower; } return directiveValue; diff --git a/utils.ts b/src/utils.ts similarity index 83% rename from utils.ts rename to src/utils.ts index b572b1f..8d92978 100644 --- a/utils.ts +++ b/src/utils.ts @@ -17,10 +17,6 @@ * limitations under the License. */ - -import * as csp from './csp'; - - /** * Removes scheme from url. * @param url Url. @@ -40,11 +36,11 @@ export function getSchemeFreeUrl(url: string): string { */ export function getHostname(url: string): string { const hostname = new URL( - 'https://' + - getSchemeFreeUrl(url) - .replace(':*', '') // Remove wildcard port - .replace('*', 'wildcard_placeholder')) - .hostname.replace('wildcard_placeholder', '*'); + 'https://' + + getSchemeFreeUrl(url) + .replace(':*', '') // Remove wildcard port + .replace('*', 'wildcard_placeholder') + ).hostname.replace('wildcard_placeholder', '*'); // Some browsers strip the brackets from IPv6 addresses when you access the // hostname. If the scheme free url starts with something that vaguely looks @@ -72,16 +68,21 @@ function setScheme(u: string): string { * @return First match found in url list, null otherwise. */ export function matchWildcardUrls( - cspUrlString: string, listOfUrlStrings: string[]): URL|null { + cspUrlString: string, + listOfUrlStrings: string[] +): URL | null { // non-Chromium browsers don't support wildcards in domain names. We work // around this by replacing the wildcard with `wildcard_placeholder` before // parsing the domain and using that as a magic string. This magic string is // encapsulated in this function such that callers of this function do not // have to worry about this detail. - const cspUrl = - new URL(setScheme(cspUrlString - .replace(':*', '') // Remove wildcard port - .replace('*', 'wildcard_placeholder'))); + const cspUrl = new URL( + setScheme( + cspUrlString + .replace(':*', '') // Remove wildcard port + .replace('*', 'wildcard_placeholder') + ) + ); const listOfUrls = listOfUrlStrings.map(u => new URL(setScheme(u))); const host = cspUrl.hostname.toLowerCase(); const hostHasWildcard = host.startsWith('wildcard_placeholder.'); @@ -125,7 +126,6 @@ export function matchWildcardUrls( return null; } - /** * Applies a check to all directive values of a csp. * @param parsedCsp Parsed CSP. @@ -133,15 +133,21 @@ export function matchWildcardUrls( * should get applied on directive values. */ export function applyCheckFunktionToDirectives( - parsedCsp: csp.Csp, - check: (directive: string, directiveValues: string[]) => void, + parsedCsp: Record, + check: (directive: string, directiveValues: string[]) => void ) { - const directiveNames = Object.keys(parsedCsp.directives); + const directiveNames = Object.keys(parsedCsp); for (const directive of directiveNames) { - const directiveValues = parsedCsp.directives[directive]; + const directiveValues = parsedCsp[directive]; if (directiveValues) { check(directive, directiveValues); } } } + +export function mergeCspHeaders(headerValues: string[]): string { + return headerValues + .map(Function.prototype.call, String.prototype.trim) + .join(', '); +} diff --git a/checks/parser_checks_test.ts b/test/checks/parser_checks_test.ts similarity index 93% rename from checks/parser_checks_test.ts rename to test/checks/parser_checks_test.ts index 0430141..55376d3 100644 --- a/checks/parser_checks_test.ts +++ b/test/checks/parser_checks_test.ts @@ -19,11 +19,11 @@ import 'jasmine'; -import {Finding, Severity} from '../finding'; -import {CspParser} from '../parser'; +import { Finding, Severity } from '../../src/finding'; +import { CspParser } from '../../src/parser'; -import {CheckerFunction} from './checker'; -import * as parserChecks from './parser_checks'; +import { CheckerFunction } from '../../src/checks/checker'; +import * as parserChecks from '../../src/checks/parser_checks'; /** * Runs a check on a CSP string. diff --git a/checks/security_checks_test.ts b/test/checks/security_checks_test.ts similarity index 64% rename from checks/security_checks_test.ts rename to test/checks/security_checks_test.ts index 71a6098..353f2c8 100644 --- a/checks/security_checks_test.ts +++ b/test/checks/security_checks_test.ts @@ -18,12 +18,12 @@ */ - import {Directive, Version} from '../csp'; - import {Finding, Severity} from '../finding'; - import {CspParser} from '../parser'; + import { Directive, Version } from '../../src/csp'; + import { Finding, Severity, Type } from '../../src/finding'; + import { CspParser } from '../../src/parser'; - import {CheckerFunction} from './checker'; - import * as securityChecks from './security_checks'; + import { CheckerFunction } from '../../src/checks/checker'; + import * as securityChecks from '../../src/checks/security_checks'; /** * Helper function for running a check on a CSP string. @@ -44,6 +44,8 @@ const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); expect(violations.length).toBe(1); expect(violations[0].severity).toBe(Severity.HIGH); + expect(violations[0].type).toBe(Type.SCRIPT_UNSAFE_INLINE); + expect(violations[0].directive).toBe('script-src'); }); it('CheckScriptUnsafeInlineInDefaultSrc', () => { @@ -51,6 +53,9 @@ const violations = checkCsp(test, securityChecks.checkScriptUnsafeInline); expect(violations.length).toBe(1); + expect(violations[0].severity).toBe(Severity.HIGH); + expect(violations[0].type).toBe(Type.SCRIPT_UNSAFE_INLINE); + expect(violations[0].directive).toBe('default-src'); }); it('CheckScriptUnsafeInlineInDefaultSrcAndNotInScriptSrc', () => { @@ -63,15 +68,79 @@ it('CheckScriptUnsafeInlineWithNonce', () => { const test = 'script-src \'unsafe-inline\' \'nonce-foobar\''; const parsedCsp = (new CspParser(test)).csp; - + let effectiveCsp = parsedCsp.getEffectiveCsp(Version.CSP1); let violations = securityChecks.checkScriptUnsafeInline(effectiveCsp); expect(violations.length).toBe(1); - + effectiveCsp = parsedCsp.getEffectiveCsp(Version.CSP3); violations = securityChecks.checkScriptUnsafeInline(effectiveCsp); expect(violations.length).toBe(0); }); + + /** Tests for csp.securityChecks.checkStyleUnsafeInline */ + it('CheckStyleUnsafeInlineInStyleSrc', () => { + const test = 'default-src https:; style-src \'unsafe-inline\''; + + const violations = checkCsp(test, securityChecks.checkStyleUnsafeInline); + expect(violations.length).toBe(1); + expect(violations[0].severity).toBe(Severity.MEDIUM); + expect(violations[0].type).toBe(Type.STYLE_UNSAFE_INLINE); + expect(violations[0].directive).toBe('style-src'); + }); + + it('CheckStyleUnsafeInlineInDefaultSrc', () => { + const test = 'default-src \'unsafe-inline\'; script-src \'self\''; + + const violations = checkCsp(test, securityChecks.checkStyleUnsafeInline); + expect(violations.length).toBe(1); + expect(violations[0].severity).toBe(Severity.MEDIUM); + expect(violations[0].type).toBe(Type.STYLE_UNSAFE_INLINE); + expect(violations[0].directive).toBe('default-src'); + }); + + it('CheckStyleUnsafeInlineInDefaultSrcAndNotInScriptSrc', () => { + const test = 'default-src \'unsafe-inline\'; style-src https:'; + + const violations = checkCsp(test, securityChecks.checkStyleUnsafeInline); + expect(violations.length).toBe(0); + }); + + // TODO: Fix the hardcoded script references in the getEffectiveCsps + //it('CheckStyleUnsafeInlineWithNonce', () => { + // const test = 'style-src \'unsafe-inline\' \'nonce-foobar\''; + // const parsedCsp = (new CspParser(test)).csps; + // + // let effectiveCsp = parsedCsp.getEffectiveCsps(Version.CSP1); + // let violations = securityChecks.checkStyleUnsafeInline(effectiveCsp); + // expect(violations.length).toBe(1); + // + // effectiveCsp = parsedCsp.getEffectiveCsps(Version.CSP3); + // violations = securityChecks.checkStyleUnsafeInline(effectiveCsp); + // expect(violations.length).toBe(0); + //}); + + /** Tests for csp.securityChecks.checkScriptWasmUnsafeEval */ + it('CheckScriptWasmUnsafeEvalInScriptSrc', () => { + const test = 'default-src https:; script-src \'wasm-unsafe-eval\''; + + const violations = checkCsp(test, securityChecks.checkScriptWasmUnsafeEval); + expect(violations.length).toBe(1); + expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); + expect(violations[0].type).toBe(Type.SCRIPT_WASM_UNSAFE_EVAL); + expect(violations[0].directive).toBe('script-src'); + }); + + /** Tests for csp.securityChecks.checkScriptWasmUnsafeEval */ + it('CheckScriptWasmUnsafeEvalInDefaultSrc', () => { + const test = 'default-src \'wasm-unsafe-eval\''; + + const violations = checkCsp(test, securityChecks.checkScriptWasmUnsafeEval); + expect(violations.length).toBe(1); + expect(violations[0].severity).toBe(Severity.MEDIUM_MAYBE); + expect(violations[0].type).toBe(Type.SCRIPT_WASM_UNSAFE_EVAL); + expect(violations[0].directive).toBe('default-src'); + }); /** Tests for csp.securityChecks.checkScriptUnsafeEval */ it('CheckScriptUnsafeEvalInScriptSrc', () => { @@ -133,6 +202,29 @@ expect(violations.length).toBe(0); }); + /** Tests for csp.securityChecks.checkPlainUrlSchemesInFormActions */ + it('checkPlainUrlSchemesInFormActions', () => { + const test = 'form-action http: https:'; + + const violations = checkCsp(test, securityChecks.checkPlainUrlSchemesInFormActions); + expect(violations.length).toBe(2); + expect(violations[0].severity).toBe(Severity.LOW); + expect(violations[0].type).toBe(Type.PLAIN_URL_SCHEMES); + expect(violations[0].directive).toBe('form-action'); + expect(violations[1].severity).toBe(Severity.LOW); + expect(violations[1].type).toBe(Type.PLAIN_URL_SCHEMES); + expect(violations[1].directive).toBe('form-action'); + }); + + it('checkPlainUrlSchemesInFormActionsOK', () => { + const test = + 'default-src https:; object-src \'none\'; script-src \'none\'; ' + + 'base-uri \'none\'; form-action \'none\''; + + const violations = checkCsp(test, securityChecks.checkPlainUrlSchemesInFormActions); + expect(violations.length).toBe(0); + }); + /** Tests for csp.securityChecks.checkWildcards */ it('CheckWildcardsInScriptSrc', () => { const test = 'script-src * http://* //*'; @@ -174,69 +266,108 @@ /** Tests for csp.securityChecks.checkMissingDirectives */ it('CheckMissingDirectivesMissingObjectSrc', () => { - const test = 'script-src \'none\''; + const test = 'script-src \'none\'; style-src \'none\'; form-action \'self\''; const violations = checkCsp(test, securityChecks.checkMissingDirectives); expect(violations.length).toBe(1); expect(violations[0].severity).toBe(Severity.HIGH); + expect(violations[0].type).toBe(Type.MISSING_DIRECTIVES); + expect(violations[0].directive).toBe('object-src'); }); it('CheckMissingDirectivesMissingScriptSrc', () => { - const test = 'object-src \'none\''; + const test = 'object-src \'none\'; style-src \'none\'; form-action \'self\''; const violations = checkCsp(test, securityChecks.checkMissingDirectives); expect(violations.length).toBe(1); expect(violations[0].severity).toBe(Severity.HIGH); + expect(violations[0].type).toBe(Type.MISSING_DIRECTIVES); + expect(violations[0].directive).toBe('script-src'); + }); + + it('CheckMissingDirectivesMissingStyleSrc', () => { + const test = 'script-src \'none\'; object-src \'none\'; form-action \'self\''; + + const violations = checkCsp(test, securityChecks.checkMissingDirectives); + expect(violations.length).toBe(1); + expect(violations[0].severity).toBe(Severity.LOW); + expect(violations[0].type).toBe(Type.MISSING_DIRECTIVES); + expect(violations[0].directive).toBe('style-src'); + }); + + it('CheckMissingDirectivesMissingFormAction', () => { + const test = 'script-src \'none\'; object-src \'none\'; style-src \'none\''; + + const violations = checkCsp(test, securityChecks.checkMissingDirectives); + expect(violations.length).toBe(1); + expect(violations[0].severity).toBe(Severity.LOW); + expect(violations[0].type).toBe(Type.MISSING_DIRECTIVES); + expect(violations[0].directive).toBe('form-action'); }); it('CheckMissingDirectivesObjectSrcSelf', () => { - const test = 'object-src \'self\''; + const test = 'object-src \'self\'; style-src \'none\'; form-action \'self\''; const violations = checkCsp(test, securityChecks.checkMissingDirectives); expect(violations.length).toBe(1); expect(violations[0].severity).toBe(Severity.HIGH); + expect(violations[0].type).toBe(Type.MISSING_DIRECTIVES); + expect(violations[0].directive).toBe('script-src'); }); it('CheckMissingDirectivesMissingBaseUriInNonceCsp', () => { - const test = 'script-src \'nonce-123\'; object-src \'none\''; + const test = 'script-src \'nonce-123\'; object-src \'none\'; style-src \'none\'; form-action \'self\''; const violations = checkCsp(test, securityChecks.checkMissingDirectives); expect(violations.length).toBe(1); expect(violations[0].severity).toBe(Severity.HIGH); + expect(violations[0].type).toBe(Type.MISSING_DIRECTIVES); + expect(violations[0].directive).toBe('base-uri'); }); it('CheckMissingDirectivesMissingBaseUriInHashWStrictDynamicCsp', () => { const test = - 'script-src \'sha256-123456\' \'strict-dynamic\'; object-src \'none\''; + 'script-src \'sha256-123456\' \'strict-dynamic\'; object-src \'none\'; style-src \'none\'; form-action \'self\''; const violations = checkCsp(test, securityChecks.checkMissingDirectives); expect(violations.length).toBe(1); expect(violations[0].severity).toBe(Severity.HIGH); + expect(violations[0].type).toBe(Type.MISSING_DIRECTIVES); + expect(violations[0].directive).toBe('base-uri'); }); it('CheckMissingDirectivesMissingBaseUriInHashCsp', () => { - const test = 'script-src \'sha256-123456\'; object-src \'none\''; + const test = 'script-src \'sha256-123456\'; object-src \'none\'; style-src \'none\'; form-action \'self\''; const violations = checkCsp(test, securityChecks.checkMissingDirectives); expect(violations.length).toBe(0); }); - it('CheckMissingDirectivesScriptAndObjectSrcSet', () => { - const test = 'script-src \'none\'; object-src \'none\''; + it('CheckMissingDirectivesAllSetExplicit', () => { + const test = 'script-src \'none\'; object-src \'none\'; style-src \'none\'; form-action \'self\''; const violations = checkCsp(test, securityChecks.checkMissingDirectives); expect(violations.length).toBe(0); }); it('CheckMissingDirectivesDefaultSrcSet', () => { - const test = 'default-src https:;'; + const test = 'default-src https:; form-action \'self\''; const violations = checkCsp(test, securityChecks.checkMissingDirectives); expect(violations.length).toBe(0); }); + it('CheckMissingDirectivesDefaultSrcSetNoFormAction', () => { + const test = 'default-src https:'; + + const violations = checkCsp(test, securityChecks.checkMissingDirectives); + expect(violations.length).toBe(1); + expect(violations[0].type).toBe(Type.MISSING_DIRECTIVES); + expect(violations[0].directive).toBe('form-action'); + }); + it('CheckMissingDirectivesDefaultSrcSetToNone', () => { - const test = 'default-src \'none\';'; + const test = 'default-src \'none\'; form-action \'self\''; const violations = checkCsp(test, securityChecks.checkMissingDirectives); expect(violations.length).toBe(0); @@ -424,6 +555,25 @@ expect(violations.every((v) => v.severity === Severity.MEDIUM)).toBeTrue(); }); + /** Tests for csp.securityChecks.checkSrcHttpWithHttps */ + it('CheckSrcHttp_whenMultipleReportingPolicies', () => { + const test = + 'script-src http://foo.bar https://test.com; report-uri http://test.com; report-uri https://test.com;'; + + const violations = checkCsp(test, securityChecks.checkSrcHttp); + expect(violations.length).toBe(2); + expect(violations.every((v) => v.severity === Severity.MEDIUM)).toBeTrue(); + }); + + /** Tests for csp.securityChecks.checkSrcHttps */ + it('CheckSrcHttp_whenReportingPolicyHttps', () => { + const test = + 'script-src http://foo.bar https://test.com; report-uri https://test.com'; + + const violations = checkCsp(test, securityChecks.checkSrcHttp); + expect(violations.length).toBe(1); + }); + /** Tests for csp.securityChecks.checkHasConfiguredReporting */ it('CheckHasConfiguredReporting_whenNoReporting', () => { const test = 'script-src \'nonce-aaaaaaaaaa\''; @@ -433,6 +583,7 @@ expect(violations.length).toBe(1); expect(violations[0].severity).toBe(Severity.INFO); + expect(violations[0].type).toBe(Type.REPORTING_DESTINATION_MISSING); expect(violations[0].directive).toBe('report-uri'); }); @@ -444,6 +595,7 @@ expect(violations.length).toBe(1); expect(violations[0].severity).toBe(Severity.INFO); + expect(violations[0].type).toBe(Type.REPORT_TO_ONLY); expect(violations[0].directive).toBe('report-to'); }); @@ -465,4 +617,38 @@ expect(violations.length).toBe(0); }); + + it('CheckHasConfiguredReporting_whenMultiplePolicies', () => { + const test = + 'script-src \'nonce-aaaaaaaaaa\'; report-uri url, style-src \'self\'; report-uri otheruri'; + + const violations = + checkCsp(test, securityChecks.checkHasConfiguredReporting); + + expect(violations.length).toBe(0); + }); + + it('CheckHasConfiguredReporting_whenMultiplePoliciesNoReporting', () => { + const test = + 'script-src \'nonce-aaaaaaaaaa\'; report-uri url, style-src \'self\''; + + const violations = checkCsp(test, securityChecks.checkHasConfiguredReporting); + + expect(violations.length).toBe(1); + expect(violations[0].severity).toBe(Severity.INFO); + expect(violations[0].type).toBe(Type.REPORTING_DESTINATION_MISSING); + expect(violations[0].directive).toBe('report-uri'); + }); + + it('CheckHasConfiguredReporting_whenMultiplePoliciesReportTo', () => { + const test = + 'script-src \'nonce-aaaaaaaaaa\'; report-uri url, style-src \'self\'; report-to test'; + + const violations = checkCsp(test, securityChecks.checkHasConfiguredReporting); + + expect(violations.length).toBe(1); + expect(violations[0].severity).toBe(Severity.INFO); + expect(violations[0].type).toBe(Type.REPORT_TO_ONLY); + expect(violations[0].directive).toBe('report-to'); + }); }); \ No newline at end of file diff --git a/checks/strictcsp_checks_test.ts b/test/checks/strictcsp_checks_test.ts similarity index 86% rename from checks/strictcsp_checks_test.ts rename to test/checks/strictcsp_checks_test.ts index 056c6f0..8acb8d4 100644 --- a/checks/strictcsp_checks_test.ts +++ b/test/checks/strictcsp_checks_test.ts @@ -17,11 +17,11 @@ * @author lwe@google.com (Lukas Weichselbaum) */ -import {Finding, Severity} from '../finding'; -import {CspParser} from '../parser'; +import { Finding, Severity } from '../../src/finding'; +import { CspParser } from '../../src/parser'; -import {CheckerFunction} from './checker'; -import * as strictcspChecks from './strictcsp_checks'; +import { CheckerFunction } from '../../src/checks/checker'; +import * as strictcspChecks from '../../src/checks/strictcsp_checks'; /** @@ -49,8 +49,7 @@ describe('Test strictcsp checks', () => { it('CheckStrictDynamicNotStandalone', () => { const test = 'script-src \'strict-dynamic\''; - const violations = - checkCsp(test, strictcspChecks.checkStrictDynamicNotStandalone); + const violations = checkCsp(test, strictcspChecks.checkStrictDynamicNotStandalone); expect(violations[0].severity).toBe(Severity.INFO); }); @@ -58,7 +57,7 @@ describe('Test strictcsp checks', () => { const test = 'script-src \'strict-dynamic\' \'nonce-foobar\''; const violations = - checkCsp(test, strictcspChecks.checkStrictDynamicNotStandalone); + checkCsp(test, strictcspChecks.checkStrictDynamicNotStandalone); expect(violations.length).toBe(0); }); @@ -67,7 +66,7 @@ describe('Test strictcsp checks', () => { const test = 'script-src \'nonce-test\''; const violations = - checkCsp(test, strictcspChecks.checkUnsafeInlineFallback); + checkCsp(test, strictcspChecks.checkUnsafeInlineFallback); expect(violations.length).toBe(1); expect(violations[0].severity).toBe(Severity.STRICT_CSP); }); @@ -76,7 +75,7 @@ describe('Test strictcsp checks', () => { const test = 'script-src \'nonce-test\' \'unsafe-inline\''; const violations = - checkCsp(test, strictcspChecks.checkUnsafeInlineFallback); + checkCsp(test, strictcspChecks.checkUnsafeInlineFallback); expect(violations.length).toBe(0); }); diff --git a/csp_test.ts b/test/csp_test.ts similarity index 86% rename from csp_test.ts rename to test/csp_test.ts index a7b1650..a47ed61 100644 --- a/csp_test.ts +++ b/test/csp_test.ts @@ -19,20 +19,36 @@ import 'jasmine'; -import {Directive, isDirective, isHash, isKeyword, isNonce, isUrlScheme, Keyword, Version} from './csp'; -import {CspParser} from './parser'; +import { Directive, isDirective, isHash, isKeyword, isNonce, isUrlScheme, Keyword, Version } from '../src/csp'; +import { CspParser } from '../src/parser'; describe('Test Csp', () => { it('ConvertToString', () => { const testCsp = 'default-src \'none\'; ' + 'script-src \'nonce-unsafefoobar\' \'unsafe-eval\' \'unsafe-inline\' ' + 'https://example.com/foo.js foo.bar; ' + - 'img-src \'self\' https: data: blob:; '; + 'img-src \'self\' https: data: blob:;'; const parsed = (new CspParser(testCsp)).csp; expect(parsed.convertToString()).toBe(testCsp); }); + it('ConvertToString with multiple CSPs', () => { + const testCsp1 = 'default-src \'self\' http://example.com http://example.net; ' + + 'connect-src \'none\'; '; + + const testCsp2 = 'connect-src http://example.com/; script-src http://example.com/; '; + + const expectedCsp = 'default-src \'self\' http://example.com http://example.net; ' + + 'connect-src \'none\';, connect-src http://example.com/; ' + + 'script-src http://example.com/;'; + + const parsed = (new CspParser([testCsp1, testCsp2])).csp; + + let cspStrings: string = parsed.convertToString(); + expect(cspStrings).toBe(expectedCsp); + }); + it('GetEffectiveCspVersion1', () => { const testCsp = 'default-src \'unsafe-inline\' \'strict-dynamic\' \'nonce-123\' ' + @@ -40,7 +56,7 @@ describe('Test Csp', () => { const parsed = (new CspParser(testCsp)).csp; const effectiveCsp = parsed.getEffectiveCsp(Version.CSP1); - expect(effectiveCsp.directives[Directive.DEFAULT_SRC]).toEqual([ + expect(effectiveCsp.directives[0][Directive.DEFAULT_SRC]).toEqual([ '\'unsafe-inline\'', '\'self\'' ]); expect(effectiveCsp.hasOwnProperty(Directive.REPORT_TO)).toBeFalse(); @@ -55,7 +71,7 @@ describe('Test Csp', () => { const parsed = (new CspParser(testCsp)).csp; const effectiveCsp = parsed.getEffectiveCsp(Version.CSP2); - expect(effectiveCsp.directives[Directive.DEFAULT_SRC]).toEqual([ + expect(effectiveCsp.directives[0][Directive.DEFAULT_SRC]).toEqual([ '\'nonce-123\'', '\'sha256-foobar\'', '\'self\'' ]); expect(effectiveCsp.hasOwnProperty(Directive.REPORT_TO)).toBeFalse(); @@ -70,12 +86,12 @@ describe('Test Csp', () => { const parsed = (new CspParser(testCsp)).csp; const effectiveCsp = parsed.getEffectiveCsp(Version.CSP3); - expect(effectiveCsp.directives[Directive.DEFAULT_SRC]).toEqual([ + expect(effectiveCsp.directives[0][Directive.DEFAULT_SRC]).toEqual([ '\'strict-dynamic\'', '\'nonce-123\'', '\'sha256-foobar\'' ]); - expect(effectiveCsp.directives[Directive.REPORT_TO]).toEqual(['foo.bar']); - expect(effectiveCsp.directives[Directive.WORKER_SRC]).toEqual(['*']); - expect(effectiveCsp.directives[Directive.MANIFEST_SRC]).toEqual(['*']); + expect(effectiveCsp.directives[0][Directive.REPORT_TO]).toEqual(['foo.bar']); + expect(effectiveCsp.directives[0][Directive.WORKER_SRC]).toEqual(['*']); + expect(effectiveCsp.directives[0][Directive.MANIFEST_SRC]).toEqual(['*']); }); diff --git a/evaluator_test.ts b/test/evaluator_test.ts similarity index 80% rename from evaluator_test.ts rename to test/evaluator_test.ts index 14ba17d..0762a62 100644 --- a/evaluator_test.ts +++ b/test/evaluator_test.ts @@ -19,9 +19,9 @@ import 'jasmine'; -import {Csp} from './csp'; -import {CspEvaluator} from './evaluator'; -import {Finding, Severity, Type} from './finding'; +import { CspEvaluator } from '../src/evaluator'; +import { Finding, Severity, Type } from '../src/finding'; +import { Csp } from '../src/csp'; describe('Test evaluator', () => { it('CspEvaluator', () => { @@ -31,17 +31,16 @@ describe('Test evaluator', () => { }); it('Evaluate', () => { - const fakeCsp = new (Csp)(); const fakeFinding = new (Finding)( - Type.UNKNOWN_DIRECTIVE, 'Fake description', Severity.MEDIUM, - 'fake-directive', 'fake-directive-value'); + Type.UNKNOWN_DIRECTIVE, 'Fake description', Severity.MEDIUM, 'fake-directive', 'fake-directive-value'); + const fakeVerifier = (parsedCsp: Csp) => { return [fakeFinding]; }; + const fakeCsp = new Csp(); const evaluator = new (CspEvaluator)(fakeCsp); - const findings = - evaluator.evaluate([fakeVerifier, fakeVerifier], [fakeVerifier]); + const findings = evaluator.evaluate([fakeVerifier, fakeVerifier], [fakeVerifier]); const expectedFindings = [fakeFinding, fakeFinding, fakeFinding]; expect(findings).toEqual(expectedFindings); diff --git a/finding_test.ts b/test/finding_test.ts similarity index 95% rename from finding_test.ts rename to test/finding_test.ts index 303ebea..a73c44f 100644 --- a/finding_test.ts +++ b/test/finding_test.ts @@ -19,8 +19,8 @@ import 'jasmine'; -import {Directive, Keyword} from './csp'; -import {Finding, Severity, Type} from './finding'; +import { Directive, Keyword } from '../src/csp'; +import { Finding, Severity, Type } from '../src/finding'; describe('Test finding', () => { diff --git a/lighthouse/lighthouse_checks_test.ts b/test/lighthouse/lighthouse_checks_test.ts similarity index 99% rename from lighthouse/lighthouse_checks_test.ts rename to test/lighthouse/lighthouse_checks_test.ts index 99ee292..67af64c 100644 --- a/lighthouse/lighthouse_checks_test.ts +++ b/test/lighthouse/lighthouse_checks_test.ts @@ -4,11 +4,11 @@ import 'jasmine'; - import {Csp,} from '../csp'; - import {Severity} from '../finding'; - import {CspParser} from '../parser'; + import {Csp,} from '../../src/csp'; + import {Severity} from '../../src/finding'; + import {CspParser} from '../../src/parser'; - import * as lighthouseChecks from './lighthouse_checks'; + import * as lighthouseChecks from '../../src/lighthouse/lighthouse_checks'; function parsePolicies(policies: string[]): Csp[] { return policies.map(p => (new CspParser(p)).csp); diff --git a/test/parser_test.ts b/test/parser_test.ts new file mode 100644 index 0000000..6987fb9 --- /dev/null +++ b/test/parser_test.ts @@ -0,0 +1,367 @@ +/** + * @fileoverview Tests for CSP Parser. + * @author lwe@google.com (Lukas Weichselbaum) + * + * @license + * Copyright 2016 Google Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'jasmine'; + +import {CspParser, TEST_ONLY} from '../src/parser'; + + +describe('Test parser', () => { + it('CspParser', () => { + const validCsp = // Test policy with different features from CSP2. + 'default-src \'none\';' + + 'script-src \'nonce-unsafefoobar\' \'unsafe-eval\' \'unsafe-inline\' \n' + + 'https://example.com/foo.js foo.bar; ' + + 'object-src \'none\';' + + 'img-src \'self\' https: data: blob:;' + + 'style-src \'self\' \'unsafe-inline\' \'sha256-1DCfk1NYWuHMfoobarfoobar=\';' + + 'font-src *;' + + 'child-src *.example.com:9090;' + + 'upgrade-insecure-requests;\n' + + 'report-uri /csp/test'; + + const parser = new (CspParser)(validCsp); + const parsedCsp = parser.csp; + + // check directives + const directives = Object.keys(parsedCsp.directives[0]); + const expectedDirectives = [ + 'default-src', 'script-src', 'object-src', 'img-src', 'style-src', + 'font-src', 'child-src', 'upgrade-insecure-requests', 'report-uri' + ]; + expect(expectedDirectives) + .toEqual(jasmine.arrayWithExactContents(directives)); + + // check directive values + expect(['\'none\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['default-src'] as string[])); + + expect([ + '\'nonce-unsafefoobar\'', '\'unsafe-eval\'', '\'unsafe-inline\'', + 'https://example.com/foo.js', 'foo.bar' + ]) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['script-src'] as string[])); + + expect(['\'none\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['object-src'] as string[])); + + expect(['\'self\'', 'https:', 'data:', 'blob:']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['img-src'] as string[])); + expect([ + '\'self\'', '\'unsafe-inline\'', '\'sha256-1DCfk1NYWuHMfoobarfoobar=\'' + ]) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['style-src'] as string[])); + expect(['*']).toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['font-src'] as string[])); + expect(['*.example.com:9090']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['child-src'] as string[])); + expect([]).toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['upgrade-insecure-requests'] as string[])); + expect(['/csp/test']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['report-uri'] as string[])); + }); + + it('CspParserDuplicateDirectives', () => { + const validCsp = 'default-src \'none\';' + + 'default-src foo.bar;' + + 'object-src \'none\';' + + 'OBJECT-src foo.bar;'; + + const parser = new (CspParser)(validCsp); + const parsedCsp = parser.csp; + + // check directives + const directives = Object.keys(parsedCsp.directives[0]); + const expectedDirectives = ['default-src', 'object-src']; + expect(expectedDirectives) + .toEqual(jasmine.arrayWithExactContents(directives)); + + // check directive values + expect(['\'none\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['default-src'] as string[])); + expect(['\'none\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['object-src'] as string[])); + }); + + it('CspParserMixedCaseKeywords', () => { + const validCsp = 'DEFAULT-src \'NONE\';' + // Keywords should be + // case insensetive. + 'img-src \'sElf\' HTTPS: Example.com/CaseSensitive;'; + + const parser = new (CspParser)(validCsp); + const parsedCsp = parser.csp; + + // check directives + const directives = Object.keys(parsedCsp.directives[0]); + const expectedDirectives = ['default-src', 'img-src']; + expect(expectedDirectives) + .toEqual(jasmine.arrayWithExactContents(directives)); + + // check directive values + expect(['\'none\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['default-src'] as string[])); + expect(['\'self\'', 'https:', 'Example.com/CaseSensitive']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['img-src'] as string[])); + }); + + it('NormalizeDirectiveValue', () => { + expect(TEST_ONLY.normalizeDirectiveValue('\'nOnE\'')).toBe('\'none\''); + expect(TEST_ONLY.normalizeDirectiveValue('\'nonce-aBcD\'')) + .toBe('\'nonce-aBcD\''); + expect(TEST_ONLY.normalizeDirectiveValue('\'hash-XyZ==\'')) + .toBe('\'hash-XyZ==\''); + expect(TEST_ONLY.normalizeDirectiveValue('HTTPS:')).toBe('https:'); + expect(TEST_ONLY.normalizeDirectiveValue('example.com/TEST')) + .toBe('example.com/TEST'); + }); + + it('ParseMultipleDirectivesSimple', () => { + const testCsp1 = 'default-src \'self\' http://example.com http://example.net; ' + + 'connect-src \'none\'; '; + + const testCsp2 = 'connect-src http://example.com/; script-src http://example.com/; '; + + const parsed = (new CspParser([testCsp1, testCsp2])).csp; + + // check directives + const directives1 = Object.keys(parsed.directives[0]); + const expectedDirectives1 = [ + 'default-src', 'connect-src' + ]; + + const directives2 = Object.keys(parsed.directives[1]); + const expectedDirectives2 = [ + 'connect-src', 'script-src' + ]; + + expect(expectedDirectives1).toEqual(jasmine.arrayWithExactContents(directives1)); + expect(expectedDirectives2).toEqual(jasmine.arrayWithExactContents(directives2)); + + // check directive values + expect(['\'self\'', 'http://example.com', 'http://example.net']) + .toEqual(jasmine.arrayWithExactContents( + parsed.directives[0]['default-src'] as string[])); + + expect(['\'none\'']) + .toEqual(jasmine.arrayWithExactContents( + parsed.directives[0]['connect-src'] as string[])); + + expect(['http://example.com/']) + .toEqual(jasmine.arrayWithExactContents( + parsed.directives[1]['connect-src'] as string[])); + + expect(['http://example.com/']) + .toEqual(jasmine.arrayWithExactContents( + parsed.directives[1]['script-src'] as string[])); + }); + + it('CspParserMultipleDirectivesRFC2616', () => { + const validCsp = // Test policy with different features from CSP2. + 'default-src \'none\', default-src \'nonce-foobar\';' + + 'script-src \'nonce-unsafefoobar\' \'unsafe-eval\' \'unsafe-inline\' \n' + + 'https://example.com/foo.js foo.bar, script-src https:; ' + + 'object-src \'none\';' + + 'img-src \'self\' https: data: blob:;' + + 'style-src \'self\' \'unsafe-inline\' \'sha256-1DCfk1NYWuHMfoobarfoobar=\';' + + 'font-src *;' + + 'child-src *.example.com:9090;' + + 'upgrade-insecure-requests;\n' + + 'report-uri /csp/test'; + + const parser = new (CspParser)([validCsp]); + const parsedCsp = parser.csp; + + // check directives + const directives = Object.keys(parsedCsp.directives[0]); + const expectedDirectives = [ + 'default-src' + ]; + expect(expectedDirectives) + .toEqual(jasmine.arrayWithExactContents(directives)); + + const directives2 = Object.keys(parsedCsp.directives[1]); + const expectedDirectives2 = [ + 'default-src', 'script-src' + ]; + expect(expectedDirectives2) + .toEqual(jasmine.arrayWithExactContents(directives2)); + + const directives3 = Object.keys(parsedCsp.directives[2]); + const expectedDirectives3 = [ + 'script-src', 'object-src', 'img-src', 'style-src', + 'font-src', 'child-src', 'upgrade-insecure-requests', 'report-uri' + ]; + expect(expectedDirectives3) + .toEqual(jasmine.arrayWithExactContents(directives3)); + + // check directive values + expect(['\'none\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['default-src'] as string[])); + + expect(['\'nonce-foobar\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[1]['default-src'] as string[])); + + expect([ + '\'nonce-unsafefoobar\'', '\'unsafe-eval\'', '\'unsafe-inline\'', + 'https://example.com/foo.js', 'foo.bar' + ]) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[1]['script-src'] as string[])); + + expect(['https:']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[2]['script-src'] as string[])); + + expect(['\'none\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[2]['object-src'] as string[])); + + expect(['\'self\'', 'https:', 'data:', 'blob:']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[2]['img-src'] as string[])); + expect([ + '\'self\'', '\'unsafe-inline\'', '\'sha256-1DCfk1NYWuHMfoobarfoobar=\'' + ]) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[2]['style-src'] as string[])); + + expect(['*']).toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[2]['font-src'] as string[])); + + expect(['*.example.com:9090']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[2]['child-src'] as string[])); + + expect([]).toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[2]['upgrade-insecure-requests'] as string[])); + + expect(['/csp/test']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[2]['report-uri'] as string[])); + }); + + it('CspParserMultipleDirectivesMixed', () => { + const validCsp1 = // Test policy with different features from CSP2. + 'default-src \'none\';' + + 'script-src \'nonce-unsafefoobar\' \'unsafe-eval\' \'unsafe-inline\' \n' + + 'https://example.com/foo.js foo.bar; ' + + 'object-src \'none\';' + + 'img-src \'self\' https: data: blob:;' + + 'style-src \'self\' \'unsafe-inline\' \'sha256-1DCfk1NYWuHMfoobarfoobar=\';' + + 'font-src *;' + + 'child-src *.example.com:9090;' + + 'upgrade-insecure-requests;\n' + + 'report-uri /csp/test, default-src \'self\''; + + const validCsp2 = // Test policy with different features from CSP2. + 'default-src \'nonce-foobar\';' + + 'script-src https:'; + + const parser = new (CspParser)([validCsp1, validCsp2]); + const parsedCsp = parser.csp; + + // check directives + const directives = Object.keys(parsedCsp.directives[0]); + const expectedDirectives = [ + 'default-src', 'script-src', 'object-src', 'img-src', 'style-src', + 'font-src', 'child-src', 'upgrade-insecure-requests', 'report-uri' + ]; + expect(expectedDirectives) + .toEqual(jasmine.arrayWithExactContents(directives)); + + const directives1 = Object.keys(parsedCsp.directives[1]); + const expectedDirectives1 = [ + 'default-src' + ]; + expect(expectedDirectives1) + .toEqual(jasmine.arrayWithExactContents(directives1)); + + const directives2 = Object.keys(parsedCsp.directives[2]); + const expectedDirectives2 = [ + 'default-src', 'script-src' + ]; + expect(expectedDirectives2) + .toEqual(jasmine.arrayWithExactContents(directives2)); + + // check directive values + expect(['\'none\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['default-src'] as string[])); + + expect([ + '\'nonce-unsafefoobar\'', '\'unsafe-eval\'', '\'unsafe-inline\'', + 'https://example.com/foo.js', 'foo.bar' + ]) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['script-src'] as string[])); + + expect(['\'none\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['object-src'] as string[])); + + expect(['\'self\'', 'https:', 'data:', 'blob:']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['img-src'] as string[])); + + expect([ + '\'self\'', '\'unsafe-inline\'', '\'sha256-1DCfk1NYWuHMfoobarfoobar=\'' + ]) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['style-src'] as string[])); + + expect(['*']).toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['font-src'] as string[])); + + expect(['*.example.com:9090']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['child-src'] as string[])); + + expect([]).toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['upgrade-insecure-requests'] as string[])); + + expect(['/csp/test']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[0]['report-uri'] as string[])); + + expect(['\'self\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[1]['default-src'] as string[])); + + expect(['\'nonce-foobar\'']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[2]['default-src'] as string[])); + + expect(['https:']) + .toEqual(jasmine.arrayWithExactContents( + parsedCsp.directives[2]['script-src'] as string[])); + }); +}); diff --git a/utils_test.ts b/test/utils_test.ts similarity index 84% rename from utils_test.ts rename to test/utils_test.ts index 26306eb..f3f9e10 100644 --- a/utils_test.ts +++ b/test/utils_test.ts @@ -18,7 +18,7 @@ import 'jasmine'; -import {getHostname, getSchemeFreeUrl, matchWildcardUrls} from './utils'; +import { getHostname, getSchemeFreeUrl, matchWildcardUrls, mergeCspHeaders } from '../src/utils'; const TEST_BYPASSES = [ 'https://googletagmanager.com/gtm/js', 'https://www.google.com/jsapi', @@ -144,4 +144,30 @@ describe('Test Utils', () => { it('GetHostnameIPv6WithPartialProtocol', () => { expect(getHostname('//[::1]')).toBe('[::1]'); }); + + it('MergeCspHeadersSingleHeader', () => { + expect(mergeCspHeaders( + [ + 'default-src \'none\'; script-src https:;' + ] + )).toBe('default-src \'none\'; script-src https:;'); + }); + + it('MergeCspHeadersMultipleHeader', () => { + expect(mergeCspHeaders( + [ + 'default-src \'none\'; script-src https:', + 'default-src \'self\';' + ] + )).toBe('default-src \'none\'; script-src https:, default-src \'self\';'); + }); + + it('MergeCspHeadersMultipleHeaderWithSpaces', () => { + expect(mergeCspHeaders( + [ + 'default-src \'none\'; script-src https:; ', + 'default-src \'self\'; ' + ] + )).toBe('default-src \'none\'; script-src https:;, default-src \'self\';'); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 845fb17..a5e3444 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,20 @@ { + "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { "target": "ES2020", "module": "commonjs", "declaration": true, "declarationMap": true, "sourceMap": true, + "rootDir": ".", "outDir": "dist", "removeComments": true, "strict": true, "esModuleInterop": true, - "forceConsistentCasingInFileNames": true - } -} + "forceConsistentCasingInFileNames": true + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts", + ] +} \ No newline at end of file