diff --git a/.gitignore b/.gitignore index 6e7efac..4eab973 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ !.yarn/sdks !.yarn/versions node_modules +safety-web/lib/* +test-helpers/expect-violations/bin/* diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/README.md b/README.md index 2a8ef77..43b2a4b 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,43 @@ **This is not an officially supported Google product.** +**This project is under development and is not ready for production yet.** + +safety-web is an ESLint plugin that works on TypeScript and JavaScript projects and surfaces security issues like Trusted Types violations statically. + +## Development + +This project uses yarn "modern" Berry (Yarn 4) with workspaces. To install the dependencies for all [workspaces](https://yarnpkg.com/features/workspaces): + +```bash +yarn +``` + +The commands `clean`, `build`, `lint`, `test` are defined in all workspaces. This makes it possible to run them in all workspaces: + +```bash +# Build all workspaces +yarn workspaces foreach --all run build +``` + +## safety-web unit testing + +```bash +yarn workspace eslint-plugin-safety-web run test +``` + +## unit tests + integrations tests + +```bash +yarn run unit_tests +``` + +## Updating tsetse + +The core logic behind this plugin is re-used from [tsec](https://github.com/google/tsec). The [`common`](https://github.com/google/tsec/tree/main/common) directory of tsec is mirrored in `safety-web/src/common`, as vendored dependency. + +Run tsetse_update.sh to pull the latest version of tsetse in: + +```bash +bash update_tsetse.sh +``` diff --git a/package.json b/package.json index fcf27ec..728fdc2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,17 @@ { "private": true, + "license": "Apache-2.0", + "author": "Google ISE Web Team", "workspaces": [ - "safety-web" - ] -} \ No newline at end of file + "safety-web", + "tests/*", + "test-helpers/*" + ], + "scripts": { + "unit_tests": "yarn workspace eslint-plugin-safety-web test", + "integration_tests": "yarn workspace basic-typescript-eslint9 test && yarn workspace basic-typescript-eslint8 test && yarn workspace basic-javascript-eslint9 test && yarn workspace basic-javascript-eslint8 test", + "update_integration_tests": "yarn workspace basic-typescript-eslint9 update && yarn workspace basic-typescript-eslint8 update && yarn workspace basic-javascript-eslint9 update && yarn workspace basic-javascript-eslint8 update", + "test": "yarn workspaces foreach --all run test" + }, + "packageManager": "yarn@4.3.1" +} diff --git a/safety-web/.mocharc.js b/safety-web/.mocharc.js new file mode 100644 index 0000000..0fb2ad3 --- /dev/null +++ b/safety-web/.mocharc.js @@ -0,0 +1,19 @@ +// Copyright 2024 Google LLC +// +// 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 +// +// https://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. + +module.exports = { + // Use tsx as a TypeScript node loader for Mocha https://stackoverflow.com/a/77609121 + "require": "tsx", + "extension": ["ts"], +} diff --git a/safety-web/eslint.config.mjs b/safety-web/eslint.config.mjs new file mode 100644 index 0000000..c7be90d --- /dev/null +++ b/safety-web/eslint.config.mjs @@ -0,0 +1,34 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: "tsconfig.json", // Indicates to find the closest tsconfig.json for each source file (see https://typescript-eslint.io/packages/parser#project). + tsconfigRootDir: import.meta.dirname, + }, + }, + files: ["**/*.ts"], + }, + { + rules: { + 'no-undef': 'off', + 'no-dupe-class-members': 'off', + }, + files: ['**/*.ts'], + }, + { + ignores: [ + "**/*.js", + "**/*.mjs", + "test/test_fixtures/", + "lib/", + "node_modules/", + "src/common/", // tsetse folder is linted internally. + ] + }, +); diff --git a/safety-web/package.json b/safety-web/package.json index 55c028e..56b6936 100644 --- a/safety-web/package.json +++ b/safety-web/package.json @@ -1,6 +1,37 @@ { - "name": "safety-web", + "name": "eslint-plugin-safety-web", "version": "0.1.0", + "license": "Apache-2.0", + "author": "Google ISE Web Team", + "main": "lib/src/index.js", + "files": [ + "lib/src/" + ], + "scripts": { + "clean": "tsc --build --clean", + "build": "tsc -b ./tsconfig.json", + "build:watch": "tsc -b ./tsconfig.json --watch", + "lint": "eslint", + "test": "mocha", + "test:watch": "mocha -r ts-node/register --watch --watch-files src/**/*.ts,test/**/*.ts" + }, "dependencies": { + "@typescript-eslint/parser": "^7.17.0", + "@typescript-eslint/utils": "^7.17.0", + "eslint": "^8.56.0 <9.0.0", + "tsutils": "^3.21.0", + "typescript": "^5.4.3 <5.5.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@types/chai": "^4.3.16", + "@types/mocha": "^10.0.7", + "@types/node": "^20.14.9", + "@typescript-eslint/rule-tester": "^7.17.0", + "chai": "^5.1.1", + "mocha": "^10.6.0", + "ts-node": "^10.9.2", + "tsx": "^4.16.2", + "typescript-eslint": "^7.17.0" } -} \ No newline at end of file +} diff --git a/safety-web/src/common/configured_checker.ts b/safety-web/src/common/configured_checker.ts new file mode 100644 index 0000000..60c9ffb --- /dev/null +++ b/safety-web/src/common/configured_checker.ts @@ -0,0 +1,71 @@ +// Copyright 2020 Google LLC +// +// 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 {ENABLED_RULES} from './rule_groups'; +import {Checker} from './third_party/tsetse/checker'; +import * as ts from 'typescript'; + +import { + ExemptionList, + parseExemptionConfig, + resolveExemptionConfigPath, +} from './exemption_config'; + +/** + * Create a new cheker with all enabled rules registered and the exemption list + * configured. + */ +export function getConfiguredChecker( + program: ts.Program, + host: ts.ModuleResolutionHost, +): {checker: Checker; errors: ts.Diagnostic[]} { + let exemptionList: ExemptionList | undefined = undefined; + + const exemptionConfigPath = resolveExemptionConfigPath( + program.getCompilerOptions()['configFilePath'] as string, + ); + + const errors = []; + + if (exemptionConfigPath) { + const projExemptionConfigOrErr = parseExemptionConfig(exemptionConfigPath); + if (projExemptionConfigOrErr instanceof ExemptionList) { + exemptionList = projExemptionConfigOrErr; + } else { + errors.push(...projExemptionConfigOrErr); + } + } + + // Create all enabled rules with corresponding exemption list entries. + const checker = new Checker(program, host); + const wildcardAllowListEntry = exemptionList?.get('*'); + const rules = ENABLED_RULES.map((ruleCtr) => { + const allowlistEntries = []; + const allowlistEntry = exemptionList?.get(ruleCtr.RULE_NAME); + if (allowlistEntry) { + allowlistEntries.push(allowlistEntry); + } + if (wildcardAllowListEntry) { + allowlistEntries.push(wildcardAllowListEntry); + } + return new ruleCtr({allowlistEntries}); + }); + + // Register all rules. + for (const rule of rules) { + rule.register(checker); + } + + return {checker, errors}; +} diff --git a/safety-web/src/common/exemption_config.ts b/safety-web/src/common/exemption_config.ts new file mode 100644 index 0000000..2284210 --- /dev/null +++ b/safety-web/src/common/exemption_config.ts @@ -0,0 +1,230 @@ +// Copyright 2020 Google LLC +// +// 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 glob from 'glob'; +import { + AllowlistEntry, + ExemptionReason, +} from './third_party/tsetse/allowlist'; +import * as minimatch from 'minimatch'; +import * as os from 'os'; +import * as path from 'path'; +import * as ts from 'typescript'; + +/** + * Stores exemption list configurations by rules. Supports commonly used Map + * operations. + */ +export class ExemptionList { + // Extending Map doesn't work in ES5. + private readonly map: Map; + + constructor(copyFrom?: ExemptionList) { + this.map = new Map(copyFrom?.map.entries() ?? []); + } + + get(rule: string): AllowlistEntry | undefined { + return this.map.get(rule); + } + + set(rule: string, allowlistEntry: AllowlistEntry) { + this.map.set(rule, allowlistEntry); + } + + entries() { + return this.map.entries(); + } + + get size() { + return this.map.size; + } +} + +/** Get the path of the exemption configuration file from compiler options. */ +export function resolveExemptionConfigPath( + configFilePath: string, +): string | undefined { + if (!ts.sys.fileExists(configFilePath)) { + configFilePath += ts.Extension.Json; + if (!ts.sys.fileExists(configFilePath)) { + return undefined; + } + } + + const {config} = ts.readConfigFile(configFilePath, ts.sys.readFile); + const options = config?.compilerOptions; + + const configFileDir = path.dirname(configFilePath); + + if (Array.isArray(options?.plugins)) { + for (const plugin of options.plugins as ts.PluginImport[]) { + if (plugin.name !== 'tsec') continue; + const {exemptionConfig} = plugin as {exemptionConfig?: unknown}; + if (typeof exemptionConfig === 'string') { + // Path of the exemption config is relative to the path of + // tsconfig.json. Resolve it to the absolute path. + const resolvedPath = path.resolve(configFileDir, exemptionConfig); + // Always returned a path to an existing file so that tsec won't crash. + if (ts.sys.fileExists(resolvedPath)) { + return resolvedPath; + } + } + } + } + + if (typeof config.extends === 'string') { + return resolveExemptionConfigPath( + path.resolve(configFileDir, config.extends), + ); + } + + return undefined; +} + +/** Create a Diagnostic for a JSON node from a configuration file */ +function getDiagnosticErrorFromJsonNode( + node: ts.Node, + file: ts.JsonSourceFile, + messageText: string, +): ts.Diagnostic { + const start = node.getStart(file); + const length = node.getEnd() - start; + return { + source: 'tsec', + category: ts.DiagnosticCategory.Error, + code: 21110, + file, + start, + length, + messageText, + }; +} + +/** Parse the content of the exemption configuration file. */ +export function parseExemptionConfig( + exemptionConfigPath: string, +): ExemptionList | ts.Diagnostic[] { + const errors: ts.Diagnostic[] = []; + + const jsonContent = ts.sys.readFile(exemptionConfigPath)!; + const jsonSourceFile = ts.parseJsonText(exemptionConfigPath, jsonContent); + + if (!jsonSourceFile.statements.length) { + errors.push({ + source: 'tsec', + category: ts.DiagnosticCategory.Error, + code: 21110, + file: jsonSourceFile, + start: 1, + length: undefined, + messageText: 'Invalid exemtpion list', + }); + return errors; + } + + const jsonObj = jsonSourceFile.statements[0].expression; + if (!ts.isObjectLiteralExpression(jsonObj)) { + errors.push( + getDiagnosticErrorFromJsonNode( + jsonObj, + jsonSourceFile, + 'Exemption configuration requires a value of type object', + ), + ); + return errors; + } + + const exemption = new ExemptionList(); + const baseDir = path.dirname(exemptionConfigPath); + const globOptions = {cwd: baseDir, absolute: true, silent: true}; + const isWin = os.platform() === 'win32'; + + for (const prop of jsonObj.properties) { + if (!ts.isPropertyAssignment(prop)) { + errors.push( + getDiagnosticErrorFromJsonNode( + prop, + jsonSourceFile, + 'Property assignment expected', + ), + ); + continue; + } + + if (prop.name === undefined) continue; + + if ( + !ts.isStringLiteral(prop.name) || + !prop.name.getText(jsonSourceFile).startsWith(`"`) + ) { + errors.push( + getDiagnosticErrorFromJsonNode( + prop.name, + jsonSourceFile, + 'String literal with double quotes expected', + ), + ); + continue; + } + + const ruleName = prop.name.text; + + if (!ts.isArrayLiteralExpression(prop.initializer)) { + errors.push( + getDiagnosticErrorFromJsonNode( + prop.initializer, + jsonSourceFile, + `Exemption entry '${ruleName}' requires a value of type Array`, + ), + ); + continue; + } + + const fileNames: string[] = []; + const patterns: string[] = []; + + for (const elem of prop.initializer.elements) { + if (!ts.isStringLiteral(elem)) { + errors.push( + getDiagnosticErrorFromJsonNode( + elem, + jsonSourceFile, + `Item of exemption entry '${ruleName}' requires values of type string`, + ), + ); + continue; + } + let pathLike = path.resolve(baseDir, elem.text); + if (isWin) { + pathLike = pathLike.replace(/\\/g, '/'); + } + if (glob.hasMagic(elem.text, globOptions)) { + patterns.push( + // Strip the leading and trailing '/' from the stringified regexp. + minimatch.makeRe(pathLike, {}).toString().slice(1, -1), + ); + } else { + fileNames.push(pathLike); + } + } + + exemption.set(ruleName, { + reason: ExemptionReason.UNSPECIFIED, + path: fileNames, + regexp: patterns, + }); + } + + return errors.length > 0 ? errors : exemption; +} diff --git a/safety-web/src/common/rule_configuration.ts b/safety-web/src/common/rule_configuration.ts new file mode 100644 index 0000000..71308e1 --- /dev/null +++ b/safety-web/src/common/rule_configuration.ts @@ -0,0 +1,24 @@ +// Copyright 2020 Google LLC +// +// 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 {AllowlistEntry} from './third_party/tsetse/allowlist'; + +/** + * A configuration interface passed to the rules with properties configured + * externally either via allowlist or bootstrap file. + */ +export interface RuleConfiguration { + /** A list of allowlist blocks. */ + allowlistEntries?: AllowlistEntry[]; +} diff --git a/safety-web/src/common/rule_groups.ts b/safety-web/src/common/rule_groups.ts new file mode 100644 index 0000000..1225b15 --- /dev/null +++ b/safety-web/src/common/rule_groups.ts @@ -0,0 +1,89 @@ +// Copyright 2020 Google LLC +// +// 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 {AbstractRule} from './third_party/tsetse/rule'; + +import {RuleConfiguration} from './rule_configuration'; +import {Rule as TTBanBaseHrefAssignments} from './rules/dom_security/ban_base_href_assignments'; +import {Rule as TTBanDocumentExecCommand} from './rules/dom_security/ban_document_execcommand'; +import {Rule as TTBanDocumentWriteCalls} from './rules/dom_security/ban_document_write_calls'; +import {Rule as TTBanDocumentWritelnCalls} from './rules/dom_security/ban_document_writeln_calls'; +import {Rule as TTBanDomParserParseFromString} from './rules/dom_security/ban_domparser_parsefromstring'; +import {Rule as TTBanElementInnerHTMLAssignments} from './rules/dom_security/ban_element_innerhtml_assignments'; +import {Rule as TTBanElementInsertAdjacentHTML} from './rules/dom_security/ban_element_insertadjacenthtml'; +import {Rule as TTBanElementOuterHTMLAssignments} from './rules/dom_security/ban_element_outerhtml_assignments'; +import {Rule as TTBanElementSetAttribute} from './rules/dom_security/ban_element_setattribute'; +import {Rule as TTBanEvalCalls} from './rules/dom_security/ban_eval_calls'; +import {Rule as TTBanFunctionCalls} from './rules/dom_security/ban_function_calls'; +import {Rule as TTBanIFrameSrcdocAssignments} from './rules/dom_security/ban_iframe_srcdoc_assignments'; +import {Rule as TTBanObjectDataAssignments} from './rules/dom_security/ban_object_data_assignments'; +import {Rule as TTBanRangeCreateContextualFragment} from './rules/dom_security/ban_range_createcontextualfragment'; +import {Rule as TTBanScriptAppendChildCalls} from './rules/dom_security/ban_script_appendchild_calls'; +import {Rule as TTBanScriptContentAssignments} from './rules/dom_security/ban_script_content_assignments'; +import {Rule as TTBanScriptSrcAssignments} from './rules/dom_security/ban_script_src_assignments'; +import {Rule as TTBanServiceWorkerContainerRegister} from './rules/dom_security/ban_serviceworkercontainer_register'; +import {Rule as TTBanSharedWorkerCalls} from './rules/dom_security/ban_shared_worker_calls'; +import {Rule as TTBanTrustedTypesCreatepolicy} from './rules/dom_security/ban_trustedtypes_createpolicy'; +import {Rule as TTBanWindowStringfunctiondef} from './rules/dom_security/ban_window_stringfunctiondef'; +import {Rule as TTBanWorkerCalls} from './rules/dom_security/ban_worker_calls'; +import {Rule as TTBanWorkerImportScripts} from './rules/dom_security/ban_worker_importscripts'; +import {Rule as BanLegacyConversions} from './rules/unsafe/ban_legacy_conversions'; +import {Rule as BanUncheckedConversions} from './rules/unsafe/ban_reviewed_conversions'; + +/** + * An interface unifying rules extending `AbstractRule` and those extending + * `ConfornacePatternRule`. The interface exposes rule names and make it + * possible to configure non-Bazel exemption list during rule creation. + */ +export interface RuleConstructor { + readonly RULE_NAME: string; + new (configuration?: RuleConfiguration): AbstractRule; +} + +/** Conformance rules related to Trusted Types adoption */ +export const TRUSTED_TYPES_RELATED_RULES: readonly RuleConstructor[] = [ + TTBanBaseHrefAssignments, // https://github.com/w3c/webappsec-trusted-types/issues/172 + TTBanDocumentExecCommand, + TTBanDocumentWritelnCalls, + TTBanDocumentWriteCalls, + TTBanEvalCalls, + TTBanFunctionCalls, + TTBanIFrameSrcdocAssignments, + TTBanObjectDataAssignments, + TTBanScriptAppendChildCalls, + TTBanScriptContentAssignments, + TTBanScriptSrcAssignments, + TTBanServiceWorkerContainerRegister, + TTBanSharedWorkerCalls, + TTBanTrustedTypesCreatepolicy, + TTBanWindowStringfunctiondef, + TTBanWorkerCalls, + TTBanWorkerImportScripts, + TTBanElementOuterHTMLAssignments, + TTBanElementInnerHTMLAssignments, + TTBanElementInsertAdjacentHTML, + TTBanDomParserParseFromString, + TTBanElementSetAttribute, + TTBanRangeCreateContextualFragment, + BanLegacyConversions, + BanUncheckedConversions, +]; + +/** + * Conformance rules that should be registered by the check as a compiler + * plugin. + */ +export const ENABLED_RULES: readonly RuleConstructor[] = [ + ...TRUSTED_TYPES_RELATED_RULES, +]; diff --git a/safety-web/src/common/rules/dom_security/ban_base_href_assignments.ts b/safety-web/src/common/rules/dom_security/ban_base_href_assignments.ts new file mode 100644 index 0000000..aacbfec --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_base_href_assignments.ts @@ -0,0 +1,43 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Do not modify HTMLBaseElement#href elements, as this can compromise all other efforts to sanitize unsafe URLs and lead to XSS.'; + +/** + * A Rule that looks for dynamic assignments to HTMLBaseElement#href property. + * With this property modified, every URL in the page becomes unsafe. + * Developers should avoid writing to this property. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-base-href-assignments'; + + constructor(configuration: RuleConfiguration = {}) { + super({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY_WRITE, + values: ['HTMLBaseElement.prototype.href'], + name: Rule.RULE_NAME, + ...configuration, + }); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_document_execcommand.ts b/safety-web/src/common/rules/dom_security/ban_document_execcommand.ts new file mode 100644 index 0000000..441e828 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_document_execcommand.ts @@ -0,0 +1,110 @@ +// Copyright 2020 Google LLC +// +// 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 {Allowlist} from '../../third_party/tsetse/allowlist'; +import {Checker} from '../../third_party/tsetse/checker'; +import {ErrorCode} from '../../third_party/tsetse/error_code'; +import {AbstractRule} from '../../third_party/tsetse/rule'; +import {shouldExamineNode} from '../../third_party/tsetse/util/ast_tools'; +import {isLiteral} from '../../third_party/tsetse/util/is_literal'; +import {PropertyMatcher} from '../../third_party/tsetse/util/property_matcher'; +import * as ts from 'typescript'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + "Do not use document.execCommand('insertHTML'), as this can lead to XSS."; + +function matchNode( + tc: ts.TypeChecker, + n: ts.PropertyAccessExpression | ts.ElementAccessExpression, + matcher: PropertyMatcher, +) { + if (!shouldExamineNode(n)) return; + if (!matcher.typeMatches(tc.getTypeAtLocation(n.expression))) return; + + // Check if the matched node is a call to `execCommand` and if the command + // name is a literal. We will skip matching if the command name is not in + // the blocklist. + if (!ts.isCallExpression(n.parent)) return; + if (n.parent.expression !== n) return; + // It's OK if someone provided the wrong number of arguments because the code + // will have other compiler errors. + if (n.parent.arguments.length < 1) return; + const ty = tc.getTypeAtLocation(n.parent.arguments[0]); + if ( + ty.isStringLiteral() && + ty.value.toLowerCase() !== 'inserthtml' && + isLiteral(tc, n.parent.arguments[0]) + ) { + return; + } + + return n; +} + +/** A Rule that looks for use of Document#execCommand. */ +export class Rule extends AbstractRule { + static readonly RULE_NAME = 'ban-document-execcommand'; + + readonly ruleName = Rule.RULE_NAME; + readonly code = ErrorCode.CONFORMANCE_PATTERN; + + private readonly propMatcher: PropertyMatcher; + private readonly allowlist?: Allowlist; + + constructor(configuration: RuleConfiguration = {}) { + super(); + this.propMatcher = PropertyMatcher.fromSpec( + 'Document.prototype.execCommand', + ); + if (configuration.allowlistEntries) { + this.allowlist = new Allowlist(configuration.allowlistEntries); + } + } + + register(checker: Checker) { + checker.onNamedPropertyAccess( + this.propMatcher.bannedProperty, + (c, n) => { + const node = matchNode(c.typeChecker, n, this.propMatcher); + if (node) { + checker.addFailureAtNode( + node, + errMsg, + Rule.RULE_NAME, + this.allowlist, + ); + } + }, + this.code, + ); + + checker.onStringLiteralElementAccess( + this.propMatcher.bannedProperty, + (c, n) => { + const node = matchNode(c.typeChecker, n, this.propMatcher); + if (node) { + checker.addFailureAtNode( + node, + errMsg, + Rule.RULE_NAME, + this.allowlist, + ); + } + }, + this.code, + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_document_write_calls.ts b/safety-web/src/common/rules/dom_security/ban_document_write_calls.ts new file mode 100644 index 0000000..b9f5c81 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_document_write_calls.ts @@ -0,0 +1,46 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = 'Do not use Document#write, as this can lead to XSS.'; + +/** + * A Rule that looks for use of Document#write properties. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-document-write-calls'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY, + values: ['Document.prototype.write'], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_HTML, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_document_writeln_calls.ts b/safety-web/src/common/rules/dom_security/ban_document_writeln_calls.ts new file mode 100644 index 0000000..4ad101a --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_document_writeln_calls.ts @@ -0,0 +1,46 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = 'Do not use Document#writeln, as this can lead to XSS.'; + +/** + * A Rule that looks for use of Document#writeln properties. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-document-writeln-calls'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY, + values: ['Document.prototype.writeln'], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_HTML, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_domparser_parsefromstring.ts b/safety-web/src/common/rules/dom_security/ban_domparser_parsefromstring.ts new file mode 100644 index 0000000..5ddb3fa --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_domparser_parsefromstring.ts @@ -0,0 +1,42 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Using DOMParser#parseFromString to parse untrusted input into DOM elements can lead to XSS.'; + +/** A rule that bans any use of DOMParser.prototype.parseFromString. */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-domparser-parsefromstring'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY, + values: ['DOMParser.prototype.parseFromString'], + name: Rule.RULE_NAME, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_element_innerhtml_assignments.ts b/safety-web/src/common/rules/dom_security/ban_element_innerhtml_assignments.ts new file mode 100644 index 0000000..7d161da --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_element_innerhtml_assignments.ts @@ -0,0 +1,46 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Assigning directly to Element#innerHTML can result in XSS vulnerabilities.'; + +/** + * A Rule that looks for assignments to an Element's innerHTML property. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-element-innerhtml-assignments'; + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY_WRITE, + values: ['InnerHTML.prototype.innerHTML'], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_HTML, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_element_insertadjacenthtml.ts b/safety-web/src/common/rules/dom_security/ban_element_insertadjacenthtml.ts new file mode 100644 index 0000000..d591444 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_element_insertadjacenthtml.ts @@ -0,0 +1,46 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = 'Do not use Element#insertAdjacentHTML, as this can lead to XSS.'; + +/** + * A Rule that looks for use of Element#insertAdjacentHTML properties. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-element-insertadjacenthtml'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY, + values: ['Element.prototype.insertAdjacentHTML'], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_HTML, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_element_outerhtml_assignments.ts b/safety-web/src/common/rules/dom_security/ban_element_outerhtml_assignments.ts new file mode 100644 index 0000000..a6914a7 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_element_outerhtml_assignments.ts @@ -0,0 +1,46 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Assigning directly to Element#outerHTML can result in XSS vulnerabilities.'; + +/** + * A Rule that looks for assignments to an Element's innerHTML property. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-element-outerhtml-assignments'; + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY_WRITE, + values: ['Element.prototype.outerHTML'], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_HTML, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_element_setattribute.ts b/safety-web/src/common/rules/dom_security/ban_element_setattribute.ts new file mode 100644 index 0000000..8641864 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_element_setattribute.ts @@ -0,0 +1,235 @@ +// Copyright 2020 Google LLC +// +// 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 {Allowlist} from '../../third_party/tsetse/allowlist'; +import {Checker} from '../../third_party/tsetse/checker'; +import {ErrorCode} from '../../third_party/tsetse/error_code'; +import {AbstractRule} from '../../third_party/tsetse/rule'; +import {shouldExamineNode} from '../../third_party/tsetse/util/ast_tools'; +import {isLiteral} from '../../third_party/tsetse/util/is_literal'; +import {PropertyMatcher} from '../../third_party/tsetse/util/property_matcher'; +import * as ts from 'typescript'; + +import {RuleConfiguration} from '../../rule_configuration'; + +const BANNED_APIS = [ + 'Element.prototype.setAttribute', + 'Element.prototype.setAttributeNS', + 'Element.prototype.setAttributeNode', + 'Element.prototype.setAttributeNodeNS', +]; + +/** + * Trusted Types related attribute names that should not be set through + * `setAttribute` or similar functions. + */ +export const TT_RELATED_ATTRIBUTES = new Set([ + 'src', + 'srcdoc', + 'data', + 'codebase', +]); + +/** A Rule that looks for use of Element#setAttribute and similar properties. */ +export abstract class BanSetAttributeRule extends AbstractRule { + readonly code = ErrorCode.CONFORMANCE_PATTERN; + + private readonly propMatchers: readonly PropertyMatcher[]; + private readonly allowlist?: Allowlist; + + constructor(configuration: RuleConfiguration) { + super(); + this.propMatchers = BANNED_APIS.map(PropertyMatcher.fromSpec); + if (configuration.allowlistEntries) { + this.allowlist = new Allowlist(configuration.allowlistEntries); + } + } + + protected abstract readonly errorMessage: string; + protected abstract readonly isSecuritySensitiveAttrName: ( + attr: string, + ) => boolean; + + /** + * The flag that controls whether the rule matches the "unsure" cases. For all + * rules that extends this class, only one of them should set this to true, + * otherwise we will get essentially duplicate finidngs. + */ + protected abstract readonly looseMatch: boolean; + + /** + * Check if the attribute name is a literal in a setAttribute call. We will + * skip matching if the attribute name is not in the blocklist. + */ + private isCalledWithAllowedAttribute( + typeChecker: ts.TypeChecker, + node: ts.CallExpression, + ): boolean { + // The 'setAttribute' function expects exactly two arguments: an attribute + // name and a value. It's OK if someone provided the wrong number of + // arguments because the code will have other compiler errors. + if (node.arguments.length !== 2) return true; + return this.isAllowedAttribute(typeChecker, node.arguments[0]); + } + + /** + * Check if the attribute name is a literal and the namespace is null in a + * setAttributeNS call. We will skip matching if the attribute name is not in + * the blocklist. + */ + private isCalledWithAllowedAttributeNS( + typeChecker: ts.TypeChecker, + node: ts.CallExpression, + ): boolean { + // The 'setAttributeNS' function expects exactly three arguments: a + // namespace, an attribute name and a value. It's OK if someone provided the + // wrong number of arguments because the code will have other compiler + // errors. + if (node.arguments.length !== 3) return true; + return ( + node.arguments[0].kind === ts.SyntaxKind.NullKeyword && + this.isAllowedAttribute(typeChecker, node.arguments[1]) + ); + } + + /** + * Check if the attribute name is a literal that is not in the blocklist. + */ + private isAllowedAttribute( + typeChecker: ts.TypeChecker, + attr: ts.Expression, + ): boolean { + const attrType = typeChecker.getTypeAtLocation(attr); + if (this.looseMatch) { + return ( + attrType.isStringLiteral() && + !this.isSecuritySensitiveAttrName(attrType.value.toLowerCase()) && + isLiteral(typeChecker, attr) + ); + } else { + return ( + !attrType.isStringLiteral() || + !isLiteral(typeChecker, attr) || + !this.isSecuritySensitiveAttrName(attrType.value.toLowerCase()) + ); + } + } + + private matchNode( + tc: ts.TypeChecker, + n: ts.PropertyAccessExpression | ts.ElementAccessExpression, + matcher: PropertyMatcher, + ) { + if (!shouldExamineNode(n)) { + return undefined; + } + + if (!matcher.typeMatches(tc.getTypeAtLocation(n.expression))) { + // Allowed: it is a different type. + return undefined; + } + + if (!ts.isCallExpression(n.parent)) { + // Possibly not allowed: not calling it (may be renaming it). + return this.looseMatch ? n : undefined; + } + + if (n.parent.expression !== n) { + // Possibly not allowed: calling a different function with it (may be + // renaming it). + return this.looseMatch ? n : undefined; + } + + // If the matched node is a call to `setAttribute` (not setAttributeNS, etc) + // and it's not setting a security sensitive attribute. + if (matcher.bannedProperty === 'setAttribute') { + const isAllowedAttr = this.isCalledWithAllowedAttribute(tc, n.parent); + if (this.looseMatch) { + // Allowed: it is not a security sensitive attribute. + if (isAllowedAttr) return undefined; + } else { + return isAllowedAttr ? undefined : n; + } + } + + // If the matched node is a call to `setAttributeNS` with a null namespace + // and it's not setting a security sensitive attribute. + if (matcher.bannedProperty === 'setAttributeNS') { + const isAllowedAttr = this.isCalledWithAllowedAttributeNS(tc, n.parent); + if (this.looseMatch) { + // Allowed: it is not a security sensitive attribute. + if (isAllowedAttr) return undefined; + } else { + return isAllowedAttr ? undefined : n; + } + } + + return this.looseMatch ? n : undefined; + } + + register(checker: Checker) { + for (const matcher of this.propMatchers) { + checker.onNamedPropertyAccess( + matcher.bannedProperty, + (c, n) => { + const node = this.matchNode(c.typeChecker, n, matcher); + if (node) { + checker.addFailureAtNode( + node, + this.errorMessage, + this.ruleName, + this.allowlist, + ); + } + }, + this.code, + ); + + checker.onStringLiteralElementAccess( + matcher.bannedProperty, + (c, n) => { + const node = this.matchNode(c.typeChecker, n, matcher); + if (node) { + checker.addFailureAtNode( + node, + this.errorMessage, + this.ruleName, + this.allowlist, + ); + } + }, + this.code, + ); + } + } +} + +let errMsg = + 'Do not use Element#setAttribute or similar APIs, as this can lead to XSS or cause Trusted Types violations.'; + +/** A Rule that looks for use of Element#setAttribute and similar properties. */ +export class Rule extends BanSetAttributeRule { + static readonly RULE_NAME = 'ban-element-setattribute'; + + override readonly ruleName: string = Rule.RULE_NAME; + + protected readonly errorMessage = errMsg; + protected isSecuritySensitiveAttrName = (attr: string) => + (attr.startsWith('on') && attr !== 'on') || TT_RELATED_ATTRIBUTES.has(attr); + protected readonly looseMatch = true; + + constructor(configuration: RuleConfiguration = {}) { + super(configuration); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_eval_calls.ts b/safety-web/src/common/rules/dom_security/ban_eval_calls.ts new file mode 100644 index 0000000..fe2359e --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_eval_calls.ts @@ -0,0 +1,52 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_SCRIPT} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = 'Do not use eval, as this can lead to XSS.'; + +const bannedValues = [ + 'GLOBAL|eval', +]; + +/** + * A Rule that looks for references to the built-in eval() and window.eval() + * methods. window.eval performs an indirect call to eval(), so a single check + * for eval() bans both calls. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-eval-calls'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_NAME, + values: bannedValues, + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_SCRIPT, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_function_calls.ts b/safety-web/src/common/rules/dom_security/ban_function_calls.ts new file mode 100644 index 0000000..b11a156 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_function_calls.ts @@ -0,0 +1,137 @@ +// Copyright 2020 Google LLC +// +// 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 {Allowlist} from '../../third_party/tsetse/allowlist'; +import {Checker} from '../../third_party/tsetse/checker'; +import {ErrorCode} from '../../third_party/tsetse/error_code'; +import {AbstractRule} from '../../third_party/tsetse/rule'; +import {AbsoluteMatcher} from '../../third_party/tsetse/util/absolute_matcher'; +import {shouldExamineNode} from '../../third_party/tsetse/util/ast_tools'; +import {isExpressionOfAllowedTrustedType} from '../../third_party/tsetse/util/is_trusted_type'; +import {TRUSTED_SCRIPT} from '../../third_party/tsetse/util/trusted_types_configuration'; +import * as ts from 'typescript'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = 'Constructing functions from strings can lead to XSS.'; + +/** + * A Rule that looks for calls to the constructor of Function, either directly + * or through Function.prototype.constructor. + */ +export class Rule extends AbstractRule { + static readonly RULE_NAME = 'ban-function-calls'; + readonly ruleName = Rule.RULE_NAME; + readonly code = ErrorCode.CONFORMANCE_PATTERN; + + private readonly allowTrustedTypes: boolean = true; + private readonly nameMatcher: AbsoluteMatcher; + private readonly allowlist?: Allowlist; + + constructor(configuration: RuleConfiguration = {}) { + super(); + this.nameMatcher = new AbsoluteMatcher('GLOBAL|Function'); + if (configuration?.allowlistEntries) { + this.allowlist = new Allowlist(configuration?.allowlistEntries); + } + } + + register(checker: Checker) { + const check = (c: Checker, n: ts.Node) => { + const node = this.checkNode(c.typeChecker, n, this.nameMatcher); + if (node) { + checker.addFailureAtNode(node, errMsg, Rule.RULE_NAME, this.allowlist); + } + }; + checker.onNamedIdentifier(this.nameMatcher.bannedName, check, this.code); + checker.onStringLiteralElementAccess( + 'Function', + (c, n) => { + check(c, n.argumentExpression); + }, + this.code, + ); + } + + private checkNode( + tc: ts.TypeChecker, + n: ts.Node, + matcher: AbsoluteMatcher, + ): ts.Node | undefined { + let matched: (ts.Node & {arguments: readonly ts.Expression[]}) | undefined = + undefined; + + if (!shouldExamineNode(n)) return; + if (!matcher.matches(n, tc)) return; + + if (!n.parent) return; + + // Function can be accessed through window or other globalThis objects + // through the dot or bracket syntax. Check if we are seeing one of these + // cases + if ( + (ts.isPropertyAccessExpression(n.parent) && n.parent.name === n) || + ts.isElementAccessExpression(n.parent) + ) { + n = n.parent; + } + // Additionally cover the case `(Function)('bad script')`. + // Note: there can be parentheses in every expression but we cann't afford + // to check all of them. Leave other cases unchecked until we see real + // bypasses. + if (ts.isParenthesizedExpression(n.parent)) { + n = n.parent; + } + + const parent = n.parent; + + // Check if the matched node is part of a `new Function(string)` or + // `Function(string)` expression + if (ts.isNewExpression(parent) || ts.isCallExpression(parent)) { + if (parent.expression === n && parent.arguments?.length) { + matched = parent as Exclude; + } + } else { + if (!parent.parent || !parent.parent.parent) return; + + // Check if the matched node is part of a + // `Function.prototype.constructor(string)` expression. + if ( + ts.isPropertyAccessExpression(parent) && + parent.name.text === 'prototype' && + ts.isPropertyAccessExpression(parent.parent) && + parent.parent.name.text === 'constructor' && + ts.isCallExpression(parent.parent.parent) && + parent.parent.parent.expression === parent.parent && + parent.parent.parent.arguments.length + ) { + matched = parent.parent.parent; + } + } + + // If the constructor is called with TrustedScript arguments, do not flag it + // (if the rule is confugired this way). + if ( + matched && + this.allowTrustedTypes && + matched.arguments.every((arg) => + isExpressionOfAllowedTrustedType(tc, arg, TRUSTED_SCRIPT), + ) + ) { + return; + } + + return matched; + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_iframe_srcdoc_assignments.ts b/safety-web/src/common/rules/dom_security/ban_iframe_srcdoc_assignments.ts new file mode 100644 index 0000000..daa1c0c --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_iframe_srcdoc_assignments.ts @@ -0,0 +1,49 @@ +// Copyright 2020 Google LLC +// +// 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 {Fix} from '../../third_party/tsetse/failure'; +import { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {maybeAddNamedImport} from '../../third_party/tsetse/util/fixer'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; +import * as ts from 'typescript'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Assigning directly to HTMLIFrameElement#srcdoc can result in XSS vulnerabilities.'; + +/** + * A Rule that looks for assignments to an HTMLIFrameElement's srcdoc property. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-iframe-srcdoc-assignments'; + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY_WRITE, + values: ['HTMLIFrameElement.prototype.srcdoc'], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_HTML, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_object_data_assignments.ts b/safety-web/src/common/rules/dom_security/ban_object_data_assignments.ts new file mode 100644 index 0000000..91a4bfb --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_object_data_assignments.ts @@ -0,0 +1,48 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Do not assign variables to HTMLObjectElement#data, as this can lead to XSS.'; + +/** + * A Rule that looks for dynamic assignments to an HTMLScriptElement's src + * property, and suggests using safe setters instead. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-object-data-assignments'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY_WRITE, + values: ['HTMLObjectElement.prototype.data'], + ...configuration, + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_SCRIPT_URL, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_range_createcontextualfragment.ts b/safety-web/src/common/rules/dom_security/ban_range_createcontextualfragment.ts new file mode 100644 index 0000000..71b10f6 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_range_createcontextualfragment.ts @@ -0,0 +1,47 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_HTML} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Do not use Range#createContextualFragment, as this can lead to XSS.'; + +/** + * A Rule that looks for use of Range#createContextualFragment properties. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-range-createcontextualfragment'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY, + values: ['Range.prototype.createContextualFragment'], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_HTML, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_script_appendchild_calls.ts b/safety-web/src/common/rules/dom_security/ban_script_appendchild_calls.ts new file mode 100644 index 0000000..f69878b --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_script_appendchild_calls.ts @@ -0,0 +1,39 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Do not use HTMLScriptElement#appendChild because it is similar to eval and can cause code-injection security vulnerabilities.'; + +/** A rule that bans the use of HTMLScriptElement#appendChild */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-script-appendchild-calls'; + + constructor(configuration: RuleConfiguration = {}) { + super({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY, + values: ['HTMLScriptElement.prototype.appendChild'], + name: Rule.RULE_NAME, + ...configuration, + }); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_script_content_assignments.ts b/safety-web/src/common/rules/dom_security/ban_script_content_assignments.ts new file mode 100644 index 0000000..6de580a --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_script_content_assignments.ts @@ -0,0 +1,52 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_SCRIPT} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Do not assign values to HTMLScriptElement#text or HTMLScriptElement#textContent, as this can lead to XSS.'; + +/** + * A rule that bans writing to HTMLScriptElement#text and + * HTMLScriptElement#textContent + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-script-content-assignments'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY_WRITE, + values: [ + 'HTMLScriptElement.prototype.innerText', + 'HTMLScriptElement.prototype.text', + 'HTMLScriptElement.prototype.textContent', + ], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_SCRIPT, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_script_src_assignments.ts b/safety-web/src/common/rules/dom_security/ban_script_src_assignments.ts new file mode 100644 index 0000000..cf456a9 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_script_src_assignments.ts @@ -0,0 +1,48 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Do not assign variables to HTMLScriptElement#src, as this can lead to XSS.'; + +/** + * A Rule that looks for dynamic assignments to an HTMLScriptElement's src + * property, and suggests using safe setters instead. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-script-src-assignments'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY_WRITE, + values: ['HTMLScriptElement.prototype.src'], + ...configuration, + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_SCRIPT_URL, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_serviceworkercontainer_register.ts b/safety-web/src/common/rules/dom_security/ban_serviceworkercontainer_register.ts new file mode 100644 index 0000000..bce7781 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_serviceworkercontainer_register.ts @@ -0,0 +1,47 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Do not use ServiceWorkerContainer#register, as this can lead to XSS.'; + +/** + * A Rule that looks for use of Element#insertAdjacentHTML properties. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-serviceworkercontainer-register'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY, + values: ['ServiceWorkerContainer.prototype.register'], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_SCRIPT_URL, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_shared_worker_calls.ts b/safety-web/src/common/rules/dom_security/ban_shared_worker_calls.ts new file mode 100644 index 0000000..c75a0f1 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_shared_worker_calls.ts @@ -0,0 +1,48 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Constructing shared Web Workers can cause code to be loaded from an untrusted URL.'; + +/** + * A Rule that looks for calls to create new Workers and suggests using a safe + * creator instead. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-shared-worker-calls'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_NAME, + values: ['GLOBAL|SharedWorker'], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_SCRIPT_URL, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_trustedtypes_createpolicy.ts b/safety-web/src/common/rules/dom_security/ban_trustedtypes_createpolicy.ts new file mode 100644 index 0000000..611e1e7 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_trustedtypes_createpolicy.ts @@ -0,0 +1,40 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = 'Creating a Trusted Types policy requires a security review.'; + +/** + * A rule that bans TrustedTypeProlicyFactory#createPolicy. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-trustedtypes-createpolicy'; + + constructor(configuration: RuleConfiguration = {}) { + super({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_PROPERTY, + values: ['TrustedTypePolicyFactory.prototype.createPolicy'], + name: Rule.RULE_NAME, + ...configuration, + }); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_window_stringfunctiondef.ts b/safety-web/src/common/rules/dom_security/ban_window_stringfunctiondef.ts new file mode 100644 index 0000000..6aa0e80 --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_window_stringfunctiondef.ts @@ -0,0 +1,215 @@ +// Copyright 2020 Google LLC +// +// 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. + +/** + * @fileoverview Turn on a TS security checker to ban setInverval and setTimeout + * when they are called to evaluate strings as scripts. + * + * Unlike other rules that only look at the property/name, this rule checks if + * the functions are called with strings as the first argument. Therefore, none + * of the pattern engines directly applies to this rule. We could have used + * BANNED_NAME and BANNED_PROPERTY like we did for open and eval, but it causes + * too many false positives in this case. + */ + +import {Allowlist} from '../../third_party/tsetse/allowlist'; +import {Checker} from '../../third_party/tsetse/checker'; +import {ErrorCode} from '../../third_party/tsetse/error_code'; +import {AbstractRule} from '../../third_party/tsetse/rule'; +import {AbsoluteMatcher} from '../../third_party/tsetse/util/absolute_matcher'; +import {shouldExamineNode} from '../../third_party/tsetse/util/ast_tools'; +import {isExpressionOfAllowedTrustedType} from '../../third_party/tsetse/util/is_trusted_type'; +import {PropertyMatcher} from '../../third_party/tsetse/util/property_matcher'; +import {TRUSTED_SCRIPT} from '../../third_party/tsetse/util/trusted_types_configuration'; +import * as ts from 'typescript'; + +import {RuleConfiguration} from '../../rule_configuration'; + +const BANNED_NAMES = ['GLOBAL|setInterval', 'GLOBAL|setTimeout']; + +const BANNED_PROPERTIES = [ + 'Window.prototype.setInterval', + 'Window.prototype.setTimeout', +]; + +function formatErrorMessage(bannedEntity: string): string { + let errMsg = `Do not use ${bannedEntity}, as calling it with a string argument can cause code-injection security vulnerabilities.`; + return errMsg; +} + +/** + * Checks if the APIs are called with functions (that + * won't trigger an eval-like effect) or a TrustedScript value if Trusted Types + * are enabled (and it's up to the developer to make sure it + * the value can't be misused). These patterns are safe to use, so we want to + * exclude them from the reported errors. + */ +function isUsedWithNonStringArgument(n: ts.Node, tc: ts.TypeChecker) { + const par = n.parent; + // Early return on pattern like `const st = setTimeout;` We now consider this + // pattern acceptable to reduce false positives. + if (!ts.isCallExpression(par) || par.expression !== n) return true; + // Having zero arguments will trigger other compiler errors. We should not + // bother emitting a Tsetse error. + if (par.arguments.length === 0) return true; + + const firstArgType = tc.getTypeAtLocation(par.arguments[0]); + + const isFirstArgNonString = + (firstArgType.flags & + (ts.TypeFlags.String | + ts.TypeFlags.StringLike | + ts.TypeFlags.StringLiteral)) === + 0; + if (isExpressionOfAllowedTrustedType(tc, par.arguments[0], TRUSTED_SCRIPT)) { + return true; + } + return isFirstArgNonString; +} + +function isBannedStringLiteralAccess( + n: ts.ElementAccessExpression, + tc: ts.TypeChecker, + propMatcher: PropertyMatcher, +) { + const argExp = n.argumentExpression; + return ( + propMatcher.typeMatches(tc.getTypeAtLocation(n.expression)) && + ts.isStringLiteralLike(argExp) && + argExp.text === propMatcher.bannedProperty + ); +} + +/** + * A type selector that resolves to AbsoluteMatcher or PropertyMatcher based on + * the type of AST node to be matched. + */ +type NodeMatcher = T extends ts.Identifier + ? AbsoluteMatcher + : T extends ts.PropertyAccessExpression + ? PropertyMatcher + : T extends ts.ElementAccessExpression + ? { + matches: ( + n: ts.ElementAccessExpression, + tc: ts.TypeChecker, + ) => boolean; + } + : {matches: (n: ts.Node, tc: ts.TypeChecker) => never}; + +function checkNode( + tc: ts.TypeChecker, + n: T, + matcher: NodeMatcher, +): ts.Node | undefined { + if (!shouldExamineNode(n)) return; + // TODO: go/ts54upgrade - Auto-added to unblock TS5.4 migration. + // TS2345: Argument of type 'T' is not assignable to parameter of type 'never'. + // @ts-ignore + if (!matcher.matches(n, tc)) return; + if (isUsedWithNonStringArgument(n, tc)) return; + return n; +} + +/** + * A rule that checks the uses of Window#setTimeout and Window#setInterval + * properties; it also checks the global setTimeout and setInterval functions. + */ +export class Rule extends AbstractRule { + static readonly RULE_NAME = 'ban-window-stringfunctiondef'; + readonly ruleName = Rule.RULE_NAME; + readonly code = ErrorCode.CONFORMANCE_PATTERN; + + private readonly nameMatchers: readonly AbsoluteMatcher[]; + private readonly propMatchers: readonly PropertyMatcher[]; + + private readonly allowlist?: Allowlist; + + constructor(configuration: RuleConfiguration = {}) { + super(); + this.nameMatchers = BANNED_NAMES.map((name) => new AbsoluteMatcher(name)); + this.propMatchers = BANNED_PROPERTIES.map(PropertyMatcher.fromSpec); + if (configuration?.allowlistEntries) { + this.allowlist = new Allowlist(configuration?.allowlistEntries); + } + } + + register(checker: Checker) { + // Check global names + for (const nameMatcher of this.nameMatchers) { + checker.onNamedIdentifier( + nameMatcher.bannedName, + (c, n) => { + // window.id is automatically resolved to id, so the matcher will be + // able to match it. But we don't want redundant errors. Skip the + // node if it is part of a property access expression. + if (ts.isPropertyAccessExpression(n.parent)) return; + if (ts.isQualifiedName(n.parent)) return; + + const node = checkNode(c.typeChecker, n, nameMatcher); + if (node) { + checker.addFailureAtNode( + node, + formatErrorMessage(nameMatcher.bannedName), + Rule.RULE_NAME, + this.allowlist, + ); + } + }, + this.code, + ); + } + // Check properties + for (const propMatcher of this.propMatchers) { + checker.onNamedPropertyAccess( + propMatcher.bannedProperty, + (c, n) => { + const node = checkNode(c.typeChecker, n, propMatcher); + if (node) { + checker.addFailureAtNode( + node, + formatErrorMessage( + `${propMatcher.bannedType}#${propMatcher.bannedProperty}`, + ), + Rule.RULE_NAME, + this.allowlist, + ); + } + }, + this.code, + ); + + checker.onStringLiteralElementAccess( + propMatcher.bannedProperty, + (c, n) => { + const node = checkNode(c.typeChecker, n, { + matches: () => + isBannedStringLiteralAccess(n, c.typeChecker, propMatcher), + }); + if (node) { + checker.addFailureAtNode( + node, + formatErrorMessage( + `${propMatcher.bannedType}#${propMatcher.bannedProperty}`, + ), + Rule.RULE_NAME, + this.allowlist, + ); + } + }, + this.code, + ); + } + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_worker_calls.ts b/safety-web/src/common/rules/dom_security/ban_worker_calls.ts new file mode 100644 index 0000000..5b56c1e --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_worker_calls.ts @@ -0,0 +1,48 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Constructing Web Workers can cause code to be loaded from an untrusted URL.'; + +/** + * A Rule that looks for calls to create new Workers and suggests using a safe + * creator instead. + */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-worker-calls'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_NAME, + values: ['GLOBAL|Worker'], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_SCRIPT_URL, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/dom_security/ban_worker_importscripts.ts b/safety-web/src/common/rules/dom_security/ban_worker_importscripts.ts new file mode 100644 index 0000000..913d2bd --- /dev/null +++ b/safety-web/src/common/rules/dom_security/ban_worker_importscripts.ts @@ -0,0 +1,45 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; +import {overridePatternConfig} from '../../third_party/tsetse/util/pattern_config'; +import {TRUSTED_SCRIPT_URL} from '../../third_party/tsetse/util/trusted_types_configuration'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Do not call importScripts in web workers, as this can lead to XSS.'; + +/** A Rule that bans the importScripts function in worker global scopes. */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-worker-importscripts'; + + constructor(configuration: RuleConfiguration = {}) { + super( + overridePatternConfig({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_NAME, + values: ['GLOBAL|importScripts'], + name: Rule.RULE_NAME, + allowedTrustedType: TRUSTED_SCRIPT_URL, + ...configuration, + }), + ); + } +} diff --git a/safety-web/src/common/rules/unsafe/ban_legacy_conversions.ts b/safety-web/src/common/rules/unsafe/ban_legacy_conversions.ts new file mode 100644 index 0000000..eb166e3 --- /dev/null +++ b/safety-web/src/common/rules/unsafe/ban_legacy_conversions.ts @@ -0,0 +1,56 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Use of legacy conversions to safe values requires security reviews and approval.'; + +let bannedValues = [ + '/node_modules/safevalues/restricted/legacy|legacyUnsafeHtml', + '/node_modules/safevalues/restricted/legacy|legacyUnsafeScript', + '/node_modules/safevalues/restricted/legacy|legacyUnsafeScriptUrl', + // Deprecated API, keep banning for now in case people are using an older + // version of safevalues + '/node_modules/safevalues/restricted/legacy|legacyConversionToHtml', + '/node_modules/safevalues/restricted/legacy|legacyConversionToScript', + '/node_modules/safevalues/restricted/legacy|legacyConversionToScriptUrl', + '/node_modules/safevalues/unsafe/legacy|legacyConversionToHtml', + '/node_modules/safevalues/unsafe/legacy|legacyConversionToScript', + '/node_modules/safevalues/unsafe/legacy|legacyConversionToScriptUrl', +]; + +/** A Rule that bans the use of legacy conversions to safe values. */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-legacy-conversions'; + + constructor(configuration: RuleConfiguration = {}) { + super({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + values: bannedValues, + kind: PatternKind.BANNED_NAME, + name: Rule.RULE_NAME, + ...configuration, + }); + + } + +} diff --git a/safety-web/src/common/rules/unsafe/ban_reviewed_conversions.ts b/safety-web/src/common/rules/unsafe/ban_reviewed_conversions.ts new file mode 100644 index 0000000..8abb953 --- /dev/null +++ b/safety-web/src/common/rules/unsafe/ban_reviewed_conversions.ts @@ -0,0 +1,56 @@ +// Copyright 2020 Google LLC +// +// 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 { + ConformancePatternRule, + ErrorCode, + PatternKind, +} from '../../third_party/tsetse/rules/conformance_pattern_rule'; + +import {RuleConfiguration} from '../../rule_configuration'; + +let errMsg = + 'Use of reviewed conversions to safe values requires security reviews and approval.'; + +let bannedValues = [ + '/node_modules/safevalues/restricted/reviewed|htmlSafeByReview', + '/node_modules/safevalues/restricted/reviewed|scriptSafeByReview', + '/node_modules/safevalues/restricted/reviewed|scriptUrlSafeByReview', + // Deprecated API, keep banning for now in case people are using an older + // version of safevalues + '/node_modules/safevalues/restricted/reviewed|htmlFromStringKnownToSatisfyTypeContract', + '/node_modules/safevalues/restricted/reviewed|scriptFromStringKnownToSatisfyTypeContract', + '/node_modules/safevalues/restricted/reviewed|scriptUrlFromStringKnownToSatisfyTypeContract', + '/node_modules/safevalues/unsafe/reviewed|htmlFromStringKnownToSatisfyTypeContract', + '/node_modules/safevalues/unsafe/reviewed|scriptFromStringKnownToSatisfyTypeContract', + '/node_modules/safevalues/unsafe/reviewed|scriptUrlFromStringKnownToSatisfyTypeContract', +]; + +/** A Rule that bans the use of reviewed conversions to safe values. */ +export class Rule extends ConformancePatternRule { + static readonly RULE_NAME = 'ban-reviewed-conversions'; + + constructor(configuration: RuleConfiguration = {}) { + super({ + errorCode: ErrorCode.CONFORMANCE_PATTERN, + errorMessage: errMsg, + kind: PatternKind.BANNED_NAME, + values: bannedValues, + name: Rule.RULE_NAME, + ...configuration, + }); + + } + +} diff --git a/safety-web/src/common/third_party/tsetse/allowlist.ts b/safety-web/src/common/third_party/tsetse/allowlist.ts new file mode 100644 index 0000000..0915764 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/allowlist.ts @@ -0,0 +1,97 @@ +/** + * An exemption list entry, corresponding to a logical exemption rule. Use these + * to distinguish between various logical reasons for exempting something: + * for instance, tie these to particular bugs that needed to be exempted, per + * legacy project, manually reviewed entries, and so on. + * + * Exemption lists are based on the file paths provided by the TS compiler, with + * both regexp-based checks and prefix-based checks. + * + * + * Follows the logic in + * https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/conformance.proto. + */ +export interface AllowlistEntry { + /** The category corresponding to this entry. */ + readonly reason: ExemptionReason; + /** Why is this okay to be exempted?. */ + readonly explanation?: string; + + /** + * Regexps for the paths of files that will be ignored by the + * ConformancePattern. Beware, escaping can be tricky. + */ + readonly regexp?: readonly string[]; + /** Exact path of the file that will be ignored by the ConformancePattern. */ + readonly path?: readonly string[]; +} + +/** + * The categories of exemption entries. + */ +export enum ExemptionReason { + /** No reason. */ + UNSPECIFIED, + /** Code that has to be grandfathered in (no guarantees). */ + LEGACY, + /** + * Code that does not enter the scope of this particular check (no + * guarantees). + */ + OUT_OF_SCOPE, + /** Manually reviewed exceptions (supposedly okay). */ + MANUALLY_REVIEWED +} + +/** + * A complete allowlist with all related AllowlistEntry grouped together, with + * ExemptionReason ignored since it is purely for documentary purposes. + */ +export class Allowlist { + private readonly allowlistedPaths: readonly string[] = []; + private readonly allowlistedRegExps: readonly RegExp[] = []; + // To avoid repeated computation for allowlisting queries with the same file + // path, create a memoizer to cache known results. This is useful in watch + // mode (and possible in language service) when the same files can be compiled + // repeatedly. + private readonly allowlistMemoizer = new Map(); + + constructor( + allowlistEntries?: AllowlistEntry[], removePrefixes: string[] = []) { + if (allowlistEntries) { + for (const e of allowlistEntries) { + if (e.path) { + this.allowlistedPaths = this.allowlistedPaths.concat(...e.path); + } + if (e.regexp) { + this.allowlistedRegExps = this.allowlistedRegExps.concat( + ...e.regexp.map(r => new RegExp(r))); + } + } + } + } + + isAllowlisted(filePath: string): boolean { + if (this.allowlistMemoizer.has(filePath)) { + return this.allowlistMemoizer.get(filePath)!; + } + for (const p of this.allowlistedPaths) { + if (filePath === p) { + this.allowlistMemoizer.set(filePath, true); + return true; + } + if (p.endsWith('/') && filePath.startsWith(p)) { + this.allowlistMemoizer.set(filePath, true); + return true; + } + } + for (const re of this.allowlistedRegExps) { + if (re.test(filePath)) { + this.allowlistMemoizer.set(filePath, true); + return true; + } + } + this.allowlistMemoizer.set(filePath, false); + return false; + } +} diff --git a/safety-web/src/common/third_party/tsetse/checker.ts b/safety-web/src/common/third_party/tsetse/checker.ts new file mode 100644 index 0000000..dda1eb4 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/checker.ts @@ -0,0 +1,304 @@ +/** + * @fileoverview Checker contains all the information we need to perform source + * file AST traversals and report errors. + */ + +import * as ts from 'typescript'; + +import {Allowlist} from './allowlist'; +import {Failure, Fix} from './failure'; + + +/** + * A Handler contains a handler function and its corresponding error code so + * when the handler function is triggered we know which rule is violated. + */ +interface Handler { + handlerFunction(checker: Checker, node: T): void; + code: number; +} + +/** + * Tsetse rules use on() and addFailureAtNode() for rule implementations. + * Rules can get a ts.TypeChecker from checker.typeChecker so typed rules are + * possible. Compiler uses execute() to run the Tsetse check. + */ +export class Checker { + /** Node to handlers mapping for all enabled rules. */ + private readonly nodeHandlersMap = + new Map>>(); + /** + * Mapping from identifier name to handlers for all rules inspecting property + * names. + */ + private readonly namedIdentifierHandlersMap = + new Map>>(); + /** + * Mapping from property name to handlers for all rules inspecting property + * accesses expressions. + */ + private readonly namedPropertyAccessHandlersMap = + new Map>>(); + /** + * Mapping from string literal value to handlers for all rules inspecting + * string literals. + */ + private readonly stringLiteralElementAccessHandlersMap = + new Map>>(); + + private failures: Failure[] = []; + private exemptedFailures: Failure[] = []; + + private currentSourceFile: ts.SourceFile|undefined; + // currentCode will be set before invoking any handler functions so the value + // initialized here is never used. + private currentCode = 0; + + private readonly options: ts.CompilerOptions; + + /** Allow typed rules via typeChecker. */ + typeChecker: ts.TypeChecker; + + constructor( + program: ts.Program, private readonly host: ts.ModuleResolutionHost) { + // Avoid the cost for each rule to create a new TypeChecker. + this.typeChecker = program.getTypeChecker(); + this.options = program.getCompilerOptions(); + } + + /** + * This doesn't run any checks yet. Instead, it registers `handlerFunction` on + * `nodeKind` node in `nodeHandlersMap` map. After all rules register their + * handlers, the source file AST will be traversed. + */ + on( + nodeKind: T['kind'], handlerFunction: (checker: Checker, node: T) => void, + code: number) { + const newHandler: Handler = {handlerFunction, code}; + const registeredHandlers = this.nodeHandlersMap.get(nodeKind); + if (registeredHandlers === undefined) { + this.nodeHandlersMap.set(nodeKind, [newHandler]); + } else { + registeredHandlers.push(newHandler); + } + } + + /** + * Similar to `on`, but registers handlers on more specific node type, i.e., + * identifiers. + */ + onNamedIdentifier( + identifierName: string, + handlerFunction: (checker: Checker, node: ts.Identifier) => void, + code: number) { + const newHandler: Handler = {handlerFunction, code}; + const registeredHandlers = + this.namedIdentifierHandlersMap.get(identifierName); + if (registeredHandlers === undefined) { + this.namedIdentifierHandlersMap.set(identifierName, [newHandler]); + } else { + registeredHandlers.push(newHandler); + } + } + + /** + * Similar to `on`, but registers handlers on more specific node type, i.e., + * property access expressions. + */ + onNamedPropertyAccess( + propertyName: string, + handlerFunction: + (checker: Checker, node: ts.PropertyAccessExpression) => void, + code: number) { + const newHandler: + Handler = {handlerFunction, code}; + const registeredHandlers = + this.namedPropertyAccessHandlersMap.get(propertyName); + if (registeredHandlers === undefined) { + this.namedPropertyAccessHandlersMap.set(propertyName, [newHandler]); + } else { + registeredHandlers.push(newHandler); + } + } + + /** + * Similar to `on`, but registers handlers on more specific node type, i.e., + * element access expressions with string literals as keys. + */ + onStringLiteralElementAccess( + key: string, + handlerFunction: + (checker: Checker, node: ts.ElementAccessExpression) => void, + code: number) { + const newHandler: + Handler = {handlerFunction, code}; + const registeredHandlers = + this.stringLiteralElementAccessHandlersMap.get(key); + if (registeredHandlers === undefined) { + this.stringLiteralElementAccessHandlersMap.set(key, [newHandler]); + } else { + registeredHandlers.push(newHandler); + } + } + + /** + * Add a failure with a span. + * @param source the origin of the failure, e.g., the name of a rule reporting + * the failure + * @param fixes optional, automatically generated fixes that can remediate the + * failure + */ + addFailure( + start: number, end: number, failureText: string, source: string|undefined, + allowlist: Allowlist|undefined, fixes?: Fix[], + relatedInformation?: ts.DiagnosticRelatedInformation[]) { + if (!this.currentSourceFile) { + throw new Error('Source file not defined'); + } + if (start > end || end > this.currentSourceFile.end || start < 0) { + // Since only addFailureAtNode() is exposed for now this shouldn't happen. + throw new Error( + `Invalid start and end position: [${start}, ${end}]` + + ` in file ${this.currentSourceFile.fileName}.`); + } + + const failure = new Failure( + this.currentSourceFile, start, end, failureText, this.currentCode, + source, fixes ?? [], relatedInformation); + + let filePath = this.currentSourceFile.fileName; + const isFailureAllowlisted = allowlist?.isAllowlisted(filePath); + const failures = + isFailureAllowlisted ? this.exemptedFailures : this.failures; + + failures.push(failure); + } + + addFailureAtNode( + node: ts.Node, failureText: string, source: string|undefined, + allowlist: Allowlist|undefined, fixes?: Fix[], + relatedInformation?: ts.DiagnosticRelatedInformation[]) { + // node.getStart() takes a sourceFile as argument whereas node.getEnd() + // doesn't need it. + this.addFailure( + node.getStart(this.currentSourceFile), node.getEnd(), failureText, + source, allowlist, fixes, relatedInformation); + } + + createRelatedInformation(node: ts.Node, messageText: string): + ts.DiagnosticRelatedInformation { + if (!this.currentSourceFile) { + throw new Error('Source file not defined'); + } + const start = node.getStart(this.currentSourceFile); + return { + category: ts.DiagnosticCategory.Error, + code: this.currentCode, + file: this.currentSourceFile, + start, + length: node.getEnd() - start, + messageText, + }; + } + + /** Dispatch general handlers registered via `on` */ + dispatchNodeHandlers(node: ts.Node) { + const handlers = this.nodeHandlersMap.get(node.kind); + if (handlers === undefined) return; + + for (const handler of handlers) { + this.currentCode = handler.code; + handler.handlerFunction(this, node); + } + } + + /** Dispatch identifier handlers registered via `onNamedIdentifier` */ + dispatchNamedIdentifierHandlers(id: ts.Identifier) { + const handlers = this.namedIdentifierHandlersMap.get(id.text); + if (handlers === undefined) return; + + for (const handler of handlers) { + this.currentCode = handler.code; + handler.handlerFunction(this, id); + } + } + + /** + * Dispatch property access handlers registered via `onNamedPropertyAccess` + */ + dispatchNamedPropertyAccessHandlers(prop: ts.PropertyAccessExpression) { + const handlers = this.namedPropertyAccessHandlersMap.get(prop.name.text); + if (handlers === undefined) return; + + for (const handler of handlers) { + this.currentCode = handler.code; + handler.handlerFunction(this, prop); + } + } + + /** + * Dispatch string literal handlers registered via + * `onStringLiteralElementAccess`. + */ + dispatchStringLiteralElementAccessHandlers(elem: ts.ElementAccessExpression) { + const ty = this.typeChecker.getTypeAtLocation(elem.argumentExpression); + + if (!ty.isStringLiteral()) return; + + const handlers = this.stringLiteralElementAccessHandlersMap.get(ty.value); + if (handlers === undefined) return; + + for (const handler of handlers) { + this.currentCode = handler.code; + handler.handlerFunction(this, elem); + } + } + + /** + * Walk `sourceFile`, invoking registered handlers with Checker as the first + * argument and current node as the second argument. Return failures if there + * are any. + * + * Callers of this function can request that the checker report violations + * that have been exempted by an allowlist by setting the + * `reportExemptedViolations` parameter to `true`. The function will return an + * object that contains both the exempted and unexempted failures. + */ + execute(sourceFile: ts.SourceFile): Failure[]; + execute(sourceFile: ts.SourceFile, reportExemptedViolations: false): + Failure[]; + execute(sourceFile: ts.SourceFile, reportExemptedViolations: true): + {failures: Failure[], exemptedFailures: Failure[]}; + execute(sourceFile: ts.SourceFile, reportExemptedViolations: boolean = false): + Failure[]|{failures: Failure[], exemptedFailures: Failure[]} { + const thisChecker = this; + this.currentSourceFile = sourceFile; + this.failures = []; + this.exemptedFailures = []; + run(sourceFile); + return reportExemptedViolations ? + {failures: this.failures, exemptedFailures: this.exemptedFailures} : + this.failures; + + function run(node: ts.Node) { + // Dispatch handlers registered via `on` + thisChecker.dispatchNodeHandlers(node); + + // Dispatch handlers for named identifiers and properties + if (ts.isIdentifier(node)) { + thisChecker.dispatchNamedIdentifierHandlers(node); + } else if (ts.isPropertyAccessExpression(node)) { + thisChecker.dispatchNamedPropertyAccessHandlers(node); + } else if (ts.isElementAccessExpression(node)) { + thisChecker.dispatchStringLiteralElementAccessHandlers(node); + } + + ts.forEachChild(node, run); + } + } + + resolveModuleName(moduleName: string, sourceFile: ts.SourceFile) { + return ts.resolveModuleName( + moduleName, sourceFile.fileName, this.options, this.host); + } +} diff --git a/safety-web/src/common/third_party/tsetse/error_code.ts b/safety-web/src/common/third_party/tsetse/error_code.ts new file mode 100644 index 0000000..67bf2a2 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/error_code.ts @@ -0,0 +1,21 @@ +/** + * Error codes for tsetse checks. + * + * Start with 21222 and increase linearly. + * The intent is for these codes to be fixed, so that tsetse users can + * search for them in user forums and other media. + */ +export enum ErrorCode { + CHECK_RETURN_VALUE = 21222, + EQUALS_NAN = 21223, + BAN_EXPECT_TRUTHY_PROMISE = 21224, + MUST_USE_PROMISES = 21225, + BAN_PROMISE_AS_CONDITION = 21226, + PROPERTY_RENAMING_SAFE = 21227, + CONFORMANCE_PATTERN = 21228, + BAN_MUTABLE_EXPORTS = 21229, + BAN_STRING_INITIALIZED_SETS = 21230, + BAD_SIDE_EFFECT_IMPORT = 21232, + BAN_AT_INTERNAL = 21233, + ISOLATED_DECORATOR_METADATA = 21234, +} diff --git a/safety-web/src/common/third_party/tsetse/failure.ts b/safety-web/src/common/third_party/tsetse/failure.ts new file mode 100644 index 0000000..8b76f4b --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/failure.ts @@ -0,0 +1,241 @@ +import * as ts from 'typescript'; + +/** + * A Tsetse check Failure is almost identical to a Diagnostic from TypeScript + * except that: + * (1) The error code is defined by each individual Tsetse rule. + * (2) The optional `source` property is set to `Tsetse` so the host (VS Code + * for instance) would use that to indicate where the error comes from. + * (3) There's an optional suggestedFixes field. + */ +export class Failure { + constructor( + private readonly sourceFile: ts.SourceFile, + private readonly start: number, private readonly end: number, + private readonly failureText: string, private readonly code: number, + /** + * The origin of the failure, e.g., the name of the rule reporting the + * failure. Can be empty. + */ + private readonly failureSource: string|undefined, + private readonly suggestedFixes: Fix[] = [], + private readonly relatedInformation?: ts.DiagnosticRelatedInformation[]) { + } + + /** + * This returns a structure compatible with ts.Diagnostic, but with added + * fields, for convenience and to support suggested fixes. + */ + toDiagnostic(): DiagnosticWithFixes { + return { + file: this.sourceFile, + start: this.start, + end: this.end, // Not in ts.Diagnostic, but always useful for + // start-end-using systems. + length: this.end - this.start, + // Emebed `failureSource` into the error message so that we can show + // people which check they are violating. This makes it easier for + // developers to configure exemptions. + messageText: this.failureSource ? + `[${this.failureSource}] ${this.failureText}` : + this.failureText, + category: ts.DiagnosticCategory.Error, + code: this.code, + // Other tools like TSLint can use this field to decide the subcategory of + // the diagnostic. + source: this.failureSource, + fixes: this.suggestedFixes, + relatedInformation: this.relatedInformation, + }; + } + + /** + * Same as toDiagnostic, but include the fix in the message, so that systems + * that don't support displaying suggested fixes can still surface that + * information. This assumes the diagnostic message is going to be presented + * within the context of the problematic code. + */ + toDiagnosticWithStringifiedFixes(): DiagnosticWithFixes { + const diagnostic = this.toDiagnostic(); + if (this.suggestedFixes.length) { + // Add a space between the violation text and fix message. + diagnostic.messageText += ' ' + this.mapFixesToReadableString(); + } + return diagnostic; + } + + toString(): string { + return `{ sourceFile:${ + this.sourceFile ? this.sourceFile.fileName : 'unknown'}, start:${ + this.start}, end:${this.end}, source:${this.failureSource}, fixes:${ + JSON.stringify(this.suggestedFixes.map(fix => fixToString(fix)))} }`; + } + + /*** + * Stringifies an array of `suggestedFixes` for this failure. This is just a + * heuristic and should be used in systems which do not support fixers + * integration (e.g. CLI tools). + */ + mapFixesToReadableString(): string { + const stringifiedFixes = + this.suggestedFixes.map(fix => this.fixToReadableString(fix)) + .join('\nOR\n'); + + if (!stringifiedFixes) return ''; + else if (this.suggestedFixes.length === 1) { + return 'Suggested fix:\n' + stringifiedFixes; + } else { + return 'Suggested fixes:\n' + stringifiedFixes; + } + } + + /** + * Stringifies a `Fix`, in a way that makes sense when presented alongside the + * finding. This is a heuristic, obviously. + */ + fixToReadableString(f: Fix) { + let fixText = ''; + + for (const c of f.changes) { + // Remove leading/trailing whitespace from the stringified suggestions: + // since we add line breaks after each line of stringified suggestion, and + // since users will manually apply the fix, there is no need to show + // trailing whitespace. This is however just for stringification of the + // fixes: the suggested fix itself still keeps trailing whitespace. + const printableReplacement = c.replacement.trim(); + + // Insertion. + if (c.start === c.end) { + // Try to see if that's an import. + if (c.replacement.indexOf('import') !== -1) { + fixText += `- Add new import: ${printableReplacement}\n`; + } else { + // Insertion that's not a full import. This should rarely happen in + // our context, and we don't have a great message for these. + // For instance, this could be the addition of a new symbol in an + // existing import (`import {foo}` becoming `import {foo, bar}`). + fixText += `- Insert ${this.readableRange(c.start, c.end)}: ${ + printableReplacement}\n`; + } + } else if (c.start === this.start && c.end === this.end) { + // We assume the replacement is the main part of the fix, so put that + // individual change first in `fixText`. + if (printableReplacement === '') { + fixText = `- Delete the full match\n` + fixText; + } else { + fixText = `- Replace the full match with: ${printableReplacement}\n` + + fixText; + } + } else { + if (printableReplacement === '') { + fixText = + `- Delete ${this.readableRange(c.start, c.end)}\n` + fixText; + } else { + fixText = `- Replace ${this.readableRange(c.start, c.end)} with: ` + + `${printableReplacement}\n${fixText}`; + } + } + } + + return fixText.trim(); + } + + /** + * Turns the range to a human readable format to be used by fixers. + * + * If the length of the range is non zero it returns the source file text + * representing the range. Otherwise returns the stringified representation of + * the source file position. + */ + readableRange(from: number, to: number) { + const lcf = this.sourceFile.getLineAndCharacterOfPosition(from); + const lct = this.sourceFile.getLineAndCharacterOfPosition(to); + if (lcf.line === lct.line && lcf.character === lct.character) { + return `at line ${lcf.line + 1}, char ${lcf.character + 1}`; + } else { + return `'${ + this.sourceFile.text.substring(from, to).replace(/\n/g, '\\n')}'`; + } + } +} + +/** + * A `Fix` is a potential repair to the associated `Failure`. + */ +export interface Fix { + /** + * The individual text replacements composing that fix. + */ + changes: IndividualChange[]; +} + +/** Creates a fix that replaces the given node with the new text. */ +export function replaceNode(node: ts.Node, replacement: string): Fix { + return { + changes: [{ + sourceFile: node.getSourceFile(), + start: node.getStart(), + end: node.getEnd(), + replacement, + }], + }; +} + +/** Creates a fix that inserts new text in front of the given node. */ +export function insertBeforeNode(node: ts.Node, insertion: string): Fix { + return { + changes: [{ + sourceFile: node.getSourceFile(), + start: node.getStart(), + end: node.getStart(), + replacement: insertion, + }], + }; +} + +/** + * An individual text replacement/insertion in a source file. Used as part of a + * `Fix`. + */ +export interface IndividualChange { + sourceFile: ts.SourceFile; + start: number; + end: number; + replacement: string; +} + +/** + * A ts.Diagnostic that might include fixes, and with an added `end` + * field for convenience. + */ +export interface DiagnosticWithFixes extends ts.Diagnostic { + end: number; + /** + * An array of fixes for a given diagnostic. + * + * Each element (fix) of the array provides a different alternative on how to + * fix the diagnostic. Every fix is self contained and indepedent to other + * fixes in the array. + * + * These fixes can be integrated into IDEs and presented to the users who can + * choose the most suitable fix. + */ + fixes: Fix[]; +} + +/** + * Stringifies a `Fix`, replacing the `ts.SourceFile` with the matching + * filename. + */ +export function fixToString(f?: Fix) { + if (!f) return 'undefined'; + return '{' + JSON.stringify(f.changes.map(ic => { + return { + start: ic.start, + end: ic.end, + replacement: ic.replacement, + fileName: ic.sourceFile.fileName + }; + })) + + '}'; +} diff --git a/safety-web/src/common/third_party/tsetse/rule.ts b/safety-web/src/common/third_party/tsetse/rule.ts new file mode 100644 index 0000000..7cc07a8 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/rule.ts @@ -0,0 +1,21 @@ +import {Checker} from './checker'; + +/** + * Tsetse rules should extend AbstractRule and provide a `register` function. + * Rules are instantiated once per compilation operation and used across many + * files. + */ +export abstract class AbstractRule { + /** + * A lower-dashed-case name for that rule. This is not used by Tsetse itself, + * but the integrators might (such as the TypeScript Bazel rules, for + * instance). + */ + abstract readonly ruleName: string; + abstract readonly code: number; + + /** + * Registers handler functions on nodes in Checker. + */ + abstract register(checker: Checker): void; +} diff --git a/safety-web/src/common/third_party/tsetse/rules/check_side_effect_import_rule.ts b/safety-web/src/common/third_party/tsetse/rules/check_side_effect_import_rule.ts new file mode 100644 index 0000000..dbc114b --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/rules/check_side_effect_import_rule.ts @@ -0,0 +1,48 @@ +import * as ts from 'typescript'; + +import {Checker} from '../checker'; +import {ErrorCode} from '../error_code'; +import {AbstractRule} from '../rule'; + +function checkImport(checker: Checker, node: ts.ImportDeclaration) { + // Check only side-effect imports as other imports are checked by TSC. + if (node.importClause !== undefined) return; + + const modSpec = node.moduleSpecifier; + if (!modSpec) return; + + // Code with syntax errors can cause moduleSpecifier to be something other + // than a string literal. Early return to avoid a crash when + // `moduleSpecifier.name` is undefined. + if (!ts.isStringLiteral(modSpec)) return; + + const sym = checker.typeChecker.getSymbolAtLocation(modSpec); + if (sym) return; + + // It is possible that getSymbolAtLocation returns undefined, but module name + // is actually resolvable - e.g. when the imported file is empty (i.e. it is a + // script, not a module). Therefore we have to check with resolveModuleName. + + const modName = modSpec.text; + const source = node.getSourceFile(); + const resolvingResult = checker.resolveModuleName(modName, source); + if (resolvingResult && resolvingResult.resolvedModule) return; + + checker.addFailureAtNode( + node, `Cannot find module`, /*source=*/ undefined, + /*allowlist*/ undefined); +} + +/** + * Checks side effects imports and adds failures on module resolution errors. + * This is an equivalent of the TS2307 error, but for side effects imports. + */ +export class Rule extends AbstractRule { + static readonly RULE_NAME = 'check-imports'; + readonly ruleName = Rule.RULE_NAME; + readonly code = ErrorCode.BAD_SIDE_EFFECT_IMPORT; + + register(checker: Checker) { + checker.on(ts.SyntaxKind.ImportDeclaration, checkImport, this.code); + } +} diff --git a/safety-web/src/common/third_party/tsetse/rules/conformance_pattern_rule.ts b/safety-web/src/common/third_party/tsetse/rules/conformance_pattern_rule.ts new file mode 100644 index 0000000..c1e95d2 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/rules/conformance_pattern_rule.ts @@ -0,0 +1,69 @@ +import {Checker} from '../checker'; +import {ErrorCode} from '../error_code'; +import {AbstractRule} from '../rule'; +import {Fixer} from '../util/fixer'; +import {PatternEngineConfig, PatternKind, PatternRuleConfig} from '../util/pattern_config'; +import {ImportedNameEngine, NameEngine} from '../util/pattern_engines/name_engine'; +import {PatternEngine} from '../util/pattern_engines/pattern_engine'; +import {PropertyEngine} from '../util/pattern_engines/property_engine'; +import {PropertyNonConstantWriteEngine} from '../util/pattern_engines/property_non_constant_write_engine'; +import {PropertyWriteEngine} from '../util/pattern_engines/property_write_engine'; + +/** + * Builds a Rule that matches a certain pattern, given as parameter, and + * that can additionally run a suggested fix generator on the matches. + * + * This is templated, mostly to ensure the nodes that have been matched + * correspond to what the Fixer expects. + */ +export class ConformancePatternRule implements AbstractRule { + readonly ruleName: string; + readonly code: number; + private readonly engine: PatternEngine; + + constructor(readonly config: PatternRuleConfig, fixers?: Fixer[]) { + this.code = config.errorCode; + // Avoid undefined rule names. + this.ruleName = config.name ?? ''; + + let engine: { + new (ruleName: string, config: PatternEngineConfig, fixers?: Fixer[]): + PatternEngine + }; + + switch (config.kind) { + case PatternKind.BANNED_PROPERTY: + engine = PropertyEngine; + break; + case PatternKind.BANNED_PROPERTY_WRITE: + engine = PropertyWriteEngine; + break; + case PatternKind.BANNED_PROPERTY_NON_CONSTANT_WRITE: + engine = PropertyNonConstantWriteEngine; + break; + case PatternKind.BANNED_NAME: + engine = NameEngine; + break; + case PatternKind.BANNED_IMPORTED_NAME: + engine = ImportedNameEngine; + break; + default: + throw new Error('Config type not recognized, or not implemented yet.'); + } + + this.engine = new engine(this.ruleName, config, fixers); + } + + register(checker: Checker) { + this.engine.register(checker); + } +} + +// Re-exported for convenience when instantiating rules. +/** + * The list of supported patterns useable in ConformancePatternRule. The + * patterns whose name match JSConformance patterns should behave similarly (see + * https://github.com/google/closure-compiler/wiki/JS-Conformance-Framework). + */ +export {PatternKind}; +export {ErrorCode}; diff --git a/safety-web/src/common/third_party/tsetse/util/absolute_matcher.ts b/safety-web/src/common/third_party/tsetse/util/absolute_matcher.ts new file mode 100644 index 0000000..24c8aa2 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/absolute_matcher.ts @@ -0,0 +1,232 @@ +import * as ts from 'typescript'; + +import { + dealias, + debugLog, + isAllowlistedNamedDeclaration, + isInStockLibraries, +} from './ast_tools'; + +const PATH_NAME_FORMAT = '[/\\.\\w\\d_\\-$]+'; +const JS_IDENTIFIER_FORMAT = '[\\w\\d_\\-$]+'; +const FQN_FORMAT = `(${JS_IDENTIFIER_FORMAT}.)*${JS_IDENTIFIER_FORMAT}`; +const GLOBAL = 'GLOBAL'; +const ANY_SYMBOL = 'ANY_SYMBOL'; +const CLOSURE = 'CLOSURE'; +/** A fqn made out of a dot-separated chain of JS identifiers. */ +const ABSOLUTE_RE = new RegExp(`^${PATH_NAME_FORMAT}\\|${FQN_FORMAT}$`); +/** + * Clutz glues js symbols to ts namespace by prepending "ಠ_ಠ.clutz.". + * We need to include this prefix when the banned name is from Closure. + */ +const CLUTZ_SYM_PREFIX = 'ಠ_ಠ.clutz.'; + +/** + * This class matches symbols given a "foo.bar.baz" name, where none of the + * steps are instances of classes. + * + * Note that this isn't smart about subclasses and types: to write a check, we + * strongly suggest finding the expected symbol in externs to find the object + * name on which the symbol was initially defined. + * + * This matcher requires a scope for the symbol, which may be `GLOBAL`, + * `ANY_SYMBOL`, `CLOSURE` or a file path filter. `CLOSURE` indicates that the + * symbol is from the JS Closure library processed by clutz. The matcher begins + * with this scope, then the separator "|", followed by the symbol name. For + * example, "GLOBAL|eval". + * + * The file filter specifies + * (part of) the path of the file in which the symbol of interest is defined. + * For example, "path/to/file.ts|foo.bar.baz". + * With this filter, only symbols named "foo.bar.baz" that are defined in a path + * that contains "path/to/file.ts" are matched. + * + * This filter is useful when mutiple symbols have the same name but + * you want to match with a specific one. For example, assume that there are + * two classes named "Foo" defined in /path/to/file0 and /path/to/file1. + * // in /path/to/file0 + * export class Foo { static bar() {return "Foo.bar in file0";} } + * + * // in /path/to/file1 + * export class Foo { static bar() {return "Foo.bar in file1";} } + * + * Suppose that these two classes are referenced in two other files. + * // in /path/to/file2 + * import {Foo} from /path/to/file0; + * Foo.bar(); + * + * // in /path/to/file3 + * import {Foo} from /path/to/file1; + * Foo.bar(); + * + * An absolute matcher "Foo.bar" without a file filter will match with both + * references to "Foo.bar" in /path/to/file2 and /path/to/file3. + * An absolute matcher "/path/to/file1|Foo.bar", however, only matches with the + * "Foo.bar()" in /path/to/file3 because that references the "Foo.bar" defined + * in /path/to/file1. + * + * Note that an absolute matcher will match with any reference to the symbol + * defined in the file(s) specified by the file filter. For example, assume that + * Foo from file1 is extended in file4. + * + * // in /path/to/file4 + * import {Foo} from /path/to/file1; + * class Moo extends Foo { static tar() {return "Moo.tar in file4";} } + * Moo.bar(); + * + * An absolute matcher "/path/to/file1|Foo.bar" matches with "Moo.bar()" because + * "bar" is defined as part of Foo in /path/to/file1. + * + * By default, the matcher won't match symbols in import statements if the + * symbol is not renamed. Machers can be optionally configured symbols in import + * statements even if it's not a named import. + */ +export class AbsoluteMatcher { + /** + * From a "path/to/file.ts|foo.bar.baz", builds a Matcher. + */ + readonly filePath: string; + readonly bannedName: string; + + constructor( + spec: string, + readonly matchImport: boolean = false, + ) { + if (!spec.match(ABSOLUTE_RE)) { + throw new Error('Malformed matcher selector.'); + } + + // JSConformance used to use a Foo.prototype.bar syntax for bar on + // instances of Foo. TS doesn't surface the prototype part in the FQN, and + // so you can't tell static `bar` on `foo` from the `bar` property/method + // on `foo`. To avoid any confusion, throw there if we see `prototype` in + // the spec: that way, it's obvious that you're not trying to match + // properties. + if (spec.match('.prototype.')) { + throw new Error( + 'Your pattern includes a .prototype, but the AbsoluteMatcher is ' + + 'meant for non-object matches. Use the PropertyMatcher instead, or ' + + 'the Property-based PatternKinds.', + ); + } + + // Split spec by the separator "|". + [this.filePath, this.bannedName] = spec.split('|', 2); + + if (this.filePath === CLOSURE) { + this.bannedName = CLUTZ_SYM_PREFIX + this.bannedName; + } + } + + matches(n: ts.Node, tc: ts.TypeChecker): boolean { + const p = n.parent; + + debugLog(() => `start matching ${n.getText()} in ${p?.getText()}`); + + if (p !== undefined) { + // Check if the node is being declared. Declaration may be imported + // without programmer being aware of. We should not alert them about that. + // Since import statments are also declarations, this has two notable + // consequences. + // - Match is negative for imports without renaming + // - Match is positive for imports with renaming, when the imported name + // is the target. Since Tsetse is flow insensitive and we don't track + // symbol aliases, the import statement is the only place we can match + // bad symbols if they get renamed. + if (isAllowlistedNamedDeclaration(p) && p.name === n) { + if (!this.matchImport || !ts.isImportSpecifier(p)) { + return false; + } + } + } + + // Get the symbol (or the one at the other end of this alias) that we're + // looking at. + const s = dealias(tc.getSymbolAtLocation(n), tc); + if (!s) { + debugLog(() => `cannot get symbol`); + return ( + this.filePath === GLOBAL && matchGoogGlobal(n, this.bannedName, tc) + ); + } + + // The TS-provided FQN tells us the full identifier, and the origin file + // in some circumstances. + const fqn = tc.getFullyQualifiedName(s); + debugLog(() => `got FQN ${fqn}`); + + // Name-based check: `getFullyQualifiedName` returns `"filename".foo.bar` or + // just `foo.bar` if the symbol is ambient. The check here should consider + // both cases. + if (!fqn.endsWith('".' + this.bannedName) && fqn !== this.bannedName) { + debugLog(() => `FQN ${fqn} doesn't match name ${this.bannedName}`); + return false; + } + + // If `ANY_SYMBOL` or `CLOSURE` is specified, it's sufficient to conclude we + // have a match. + if (this.filePath === ANY_SYMBOL || this.filePath === CLOSURE) { + return true; + } + + // If there is no declaration, the symbol is a language built-in object. + // This is a match only if `GLOBAL` is specified. + const declarations = s.getDeclarations(); + if (declarations === undefined) { + return this.filePath === GLOBAL; + } + + // No file info in the FQN means it's imported from a .d.ts declaration + // file. This can be from a core library, a JS library, or an exported local + // symbol defined in another TS target. We need to extract the name of the + // declaration file. + if (!fqn.startsWith('"')) { + if (this.filePath === GLOBAL) { + return declarations.some(isInStockLibraries); + } else { + return declarations.some((d) => { + const srcFilePath = d.getSourceFile()?.fileName; + return srcFilePath && srcFilePath.match(this.filePath); + }); + } + } else { + const last = fqn.indexOf('"', 1); + if (last === -1) { + throw new Error('Malformed fully-qualified name.'); + } + const filePath = fqn.substring(1, last); + return filePath.match(this.filePath) !== null; + } + } +} + +function matchGoogGlobal(n: ts.Node, bannedName: string, tc: ts.TypeChecker) { + if (n.parent === undefined) return false; + + let accessExpr = n.parent; + + const ids = bannedName.split('.').reverse(); + for (const id of ids) { + let memberName; + if (ts.isPropertyAccessExpression(accessExpr)) { + memberName = accessExpr.name.text; + accessExpr = accessExpr.expression; + } else if (ts.isElementAccessExpression(accessExpr)) { + const argType = tc.getTypeAtLocation(accessExpr.argumentExpression); + if (argType.isStringLiteral()) { + memberName = argType.value; + } else { + return false; + } + accessExpr = accessExpr.expression; + } else { + return false; + } + if (id !== memberName) return false; + } + + const s = dealias(tc.getSymbolAtLocation(accessExpr), tc); + if (s === undefined) return false; + + return tc.getFullyQualifiedName(s) === 'goog.global'; +} diff --git a/safety-web/src/common/third_party/tsetse/util/ast_tools.ts b/safety-web/src/common/third_party/tsetse/util/ast_tools.ts new file mode 100644 index 0000000..7e70d5c --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/ast_tools.ts @@ -0,0 +1,148 @@ +/** + * @fileoverview This is a collection of smaller utility functions to operate on + * a TypeScript AST, used by JSConformance rules and elsewhere. + */ + +import * as ts from 'typescript'; + +/** + * Triggers increased verbosity in the rules. + */ +let DEBUG = false; + +/** + * Turns on or off logging for ConformancePatternRules. + */ +export function setDebug(state: boolean) { + DEBUG = state; +} + +/** + * Debug helper. + */ +export function debugLog(msg: () => string) { + if (DEBUG) { + console.log(msg()); + } +} + +/** + * Returns `n`'s parents in order. + */ +export function parents(n: ts.Node): ts.Node[] { + const p = []; + while (n.parent) { + n = n.parent; + p.push(n); + } + return p; +} + +/** + * Searches for something satisfying the given test in `n` or its children. + */ +export function findInChildren( + n: ts.Node, test: (n: ts.Node) => boolean): boolean { + let toExplore: ts.Node[] = [n]; + let cur: ts.Node|undefined; + while ((cur = toExplore.pop())) { + if (test(cur)) { + return true; + } + // Recurse + toExplore = toExplore.concat(cur.getChildren()); + } + return false; +} + +function isOperandOfInstanceOf(n: ts.Node) { + return ts.isBinaryExpression(n.parent) && + n.parent.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword; +} + +/** + * Returns true if the pattern-based Rule should look at that node and consider + * warning there. + */ +export function shouldExamineNode(n: ts.Node) { + return !( + (n.parent && ts.isTypeNode(n.parent)) || isOperandOfInstanceOf(n) || + ts.isTypeOfExpression(n.parent) || isInStockLibraries(n)); +} + +/** + * Return whether the given Node is (or is in) a library included as default. + * We currently look for a node_modules/typescript/ prefix, but this could + * be expanded if needed. + */ +export function isInStockLibraries(n: ts.Node|ts.SourceFile): boolean { + const sourceFile = ts.isSourceFile(n) ? n : n.getSourceFile(); + if (sourceFile) { + return sourceFile.fileName.indexOf('node_modules/typescript/') !== -1; + } else { + // the node is nowhere? Consider it as part of the core libs: we can't + // do anything with it anyways, and it was likely included as default. + return true; + } +} + +/** + * Turns the given Symbol into its non-aliased version (which could be itself). + * Returns undefined if given an undefined Symbol (so you can call + * `dealias(typeChecker.getSymbolAtLocation(node))`). + */ +export function dealias( + symbol: ts.Symbol|undefined, tc: ts.TypeChecker): ts.Symbol|undefined { + if (!symbol) { + return undefined; + } + if (symbol.getFlags() & ts.SymbolFlags.Alias) { + // Note: something that has only TypeAlias is not acceptable here. + return dealias(tc.getAliasedSymbol(symbol), tc); + } + return symbol; +} + +/** + * Returns whether `n`'s parents are something indicating a type. + */ +export function isPartOfTypeDeclaration(n: ts.Node) { + return [n, ...parents(n)].some( + p => p.kind === ts.SyntaxKind.TypeReference || + p.kind === ts.SyntaxKind.TypeLiteral); +} + +/** + * Returns whether `n` is a declared name on which we do not intend to emit + * errors. + */ +export function isAllowlistedNamedDeclaration(n: ts.Node): + n is ts.VariableDeclaration|ts.ClassDeclaration|ts.FunctionDeclaration| + ts.MethodDeclaration|ts.PropertyDeclaration|ts.InterfaceDeclaration| + ts.TypeAliasDeclaration|ts.EnumDeclaration|ts.ModuleDeclaration| + ts.ImportEqualsDeclaration|ts.ExportDeclaration|ts.MissingDeclaration| + ts.ImportClause|ts.ExportSpecifier|ts.ImportSpecifier { + return ts.isVariableDeclaration(n) || ts.isClassDeclaration(n) || + ts.isFunctionDeclaration(n) || ts.isMethodDeclaration(n) || + ts.isPropertyDeclaration(n) || ts.isInterfaceDeclaration(n) || + ts.isTypeAliasDeclaration(n) || ts.isEnumDeclaration(n) || + ts.isModuleDeclaration(n) || ts.isImportEqualsDeclaration(n) || + ts.isExportDeclaration(n) || ts.isMissingDeclaration(n) || + ts.isImportClause(n) || ts.isExportSpecifier(n) || + ts.isImportSpecifier(n); +} + +/** + * If verbose, logs the given error that happened while walking n, with a + * stacktrace. + */ +export function logASTWalkError(verbose: boolean, n: ts.Node, e: Error) { + let nodeText = `[error getting name for ${JSON.stringify(n)}]`; + try { + nodeText = '"' + n.getFullText().trim() + '"'; + } catch { + } + debugLog( + () => `Walking node ${nodeText} failed with error ${e}.\n` + + `Stacktrace:\n${e.stack}`); +} diff --git a/safety-web/src/common/third_party/tsetse/util/fixer.ts b/safety-web/src/common/third_party/tsetse/util/fixer.ts new file mode 100644 index 0000000..a87eb00 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/fixer.ts @@ -0,0 +1,199 @@ +import * as ts from 'typescript'; + +import {Fix, IndividualChange} from '../failure'; +import {debugLog} from './ast_tools'; + +export {type Fix} from '../failure'; + + +/** + * A Fixer turns Nodes (that are supposed to have been matched before) into a + * Fix. This is meant to be implemented by Rule implementers (or + * ban-preset-pattern users). See also `buildReplacementFixer` for a simpler way + * of implementing a Fixer. + */ +export interface Fixer { + getFixForFlaggedNode(node: ts.Node): Fix|undefined; +} + +/** + * A simple Fixer builder based on a function that looks at a node, and + * output either nothing, or a replacement. If this is too limiting, implement + * Fixer instead. + */ +export function buildReplacementFixer( + potentialReplacementGenerator: (node: ts.Node) => + ({replaceWith: string} | undefined)): Fixer { + return { + getFixForFlaggedNode: (n: ts.Node): Fix | undefined => { + const partialFix = potentialReplacementGenerator(n); + if (!partialFix) { + return; + } + return { + changes: [{ + sourceFile: n.getSourceFile(), + start: n.getStart(), + end: n.getEnd(), + replacement: partialFix.replaceWith, + }], + }; + } + }; +} + +interface NamedImportsFromModule { + namedBindings: ts.NamedImports; + fromModule: string; +} + +interface NamespaceImportFromModule { + namedBindings: ts.NamespaceImport; + fromModule: string; +} + +// Type union is not distributive over properties so just define a new inteface +interface NamedImportBindingsFromModule { + namedBindings: ts.NamedImports|ts.NamespaceImport; + fromModule: string; +} + +/** + * Builds an IndividualChange that imports the required symbol from the given + * file under the given name. This might reimport the same thing twice in some + * cases, but it will always make it available under the right name (though + * its name might collide with other imports, as we don't currently check for + * that). + */ +export function maybeAddNamedImport( + source: ts.SourceFile, importWhat: string, fromModule: string, + importAs?: string, tazeComment?: string): IndividualChange|undefined { + const importStatements = source.statements.filter(ts.isImportDeclaration); + const importSpecifier = + importAs ? `${importWhat} as ${importAs}` : importWhat; + + // See if the original code already imported something from the right file + const importFromRightModule = + importStatements + .map(maybeParseImportNode) + // Exclude undefined + .filter( + (binding): binding is NamedImportBindingsFromModule => + binding !== undefined) + // Exclude wildcard import + .filter( + (binding): binding is NamedImportsFromModule => + ts.isNamedImports(binding.namedBindings)) + // Exlcude imports from the wrong file + .find(binding => binding.fromModule === fromModule); + + if (importFromRightModule) { + const foundRightImport = importFromRightModule.namedBindings.elements.some( + iSpec => iSpec.propertyName ? + iSpec.name.getText() === importAs && // import {foo as bar} + iSpec.propertyName.getText() === importWhat : + iSpec.name.getText() === importWhat); // import {foo} + if (!foundRightImport) { + // Insert our symbol in the list of imports from that file. + debugLog(() => `No named imports from that file, generating new fix`); + return { + start: importFromRightModule.namedBindings.elements[0].getStart(), + end: importFromRightModule.namedBindings.elements[0].getStart(), + sourceFile: source, + replacement: `${importSpecifier}, `, + }; + } + return; // Our request is already imported under the right name. + } + + // If we get here, we didn't find anything imported from the wanted file, so + // we'll need the full import string. Add it after the last import, + // and let clang-format handle the rest. + const newImportStatement = + `import {${importSpecifier}} from '${fromModule}';` + + (tazeComment ? ` ${tazeComment}\n` : `\n`); + const insertionPosition = importStatements.length ? + importStatements[importStatements.length - 1].getEnd() + 1 : + 0; + return { + start: insertionPosition, + end: insertionPosition, + sourceFile: source, + replacement: newImportStatement, + }; +} + +/** + * Builds an IndividualChange that imports the required namespace from the given + * file under the given name. This might reimport the same thing twice in some + * cases, but it will always make it available under the right name (though + * its name might collide with other imports, as we don't currently check for + * that). + */ +export function maybeAddNamespaceImport( + source: ts.SourceFile, fromModule: string, importAs: string, + tazeComment?: string): IndividualChange|undefined { + const importStatements = source.statements.filter(ts.isImportDeclaration); + + const hasTheRightImport = + importStatements + .map(maybeParseImportNode) + // Exclude undefined + .filter( + (binding): binding is NamedImportBindingsFromModule => + binding !== undefined) + // Exlcude named imports + .filter( + (binding): binding is NamespaceImportFromModule => + ts.isNamespaceImport(binding.namedBindings)) + .some( + binding => binding.fromModule === fromModule && + binding.namedBindings.name.getText() === importAs); + + if (!hasTheRightImport) { + const insertionPosition = importStatements.length ? + importStatements[importStatements.length - 1].getEnd() + 1 : + 0; + return { + start: insertionPosition, + end: insertionPosition, + sourceFile: source, + replacement: tazeComment ? + `import * as ${importAs} from '${fromModule}'; ${tazeComment}\n` : + `import * as ${importAs} from '${fromModule}';\n`, + }; + } + return; +} + +/** + * This tries to make sense of an ImportDeclaration, and returns the + * interesting parts, undefined if the import declaration is valid but not + * understandable by the checker. + */ +function maybeParseImportNode(iDecl: ts.ImportDeclaration): + NamedImportBindingsFromModule|undefined { + if (!iDecl.importClause) { + // something like import "./file"; + debugLog( + () => + `Ignoring import without imported symbol: ${iDecl.getFullText()}`); + return; + } + if (iDecl.importClause.name || !iDecl.importClause.namedBindings) { + // Seems to happen in defaults imports like import Foo from 'Bar'. + // Not much we can do with that when trying to get a hold of some + // symbols, so just ignore that line (worst case, we'll suggest another + // import style). + debugLog(() => `Ignoring import: ${iDecl.getFullText()}`); + return; + } + if (!ts.isStringLiteral(iDecl.moduleSpecifier)) { + debugLog(() => `Ignoring import whose module specifier is not literal`); + return; + } + return { + namedBindings: iDecl.importClause.namedBindings, + fromModule: iDecl.moduleSpecifier.text + }; +} diff --git a/safety-web/src/common/third_party/tsetse/util/is_expression_value_used_or_void.ts b/safety-web/src/common/third_party/tsetse/util/is_expression_value_used_or_void.ts new file mode 100644 index 0000000..d33bd1c --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/is_expression_value_used_or_void.ts @@ -0,0 +1,14 @@ +import * as tsutils from 'tsutils'; +import * as ts from 'typescript'; + +/** + * Checks whether an expression value is used, or whether it is the operand of a + * void expression. + * + * This allows the `void` operator to be used to intentionally suppress + * conformance checks. + */ +export function isExpressionValueUsedOrVoid(node: ts.CallExpression) { + return ts.isVoidExpression(node.parent) || + tsutils.isExpressionValueUsed(node); +} diff --git a/safety-web/src/common/third_party/tsetse/util/is_literal.ts b/safety-web/src/common/third_party/tsetse/util/is_literal.ts new file mode 100644 index 0000000..5f4c08c --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/is_literal.ts @@ -0,0 +1,127 @@ +import * as ts from 'typescript'; +import {findInChildren} from './ast_tools'; + +/** + * Determines if the given ts.Node is literal enough for security purposes. This + * is true when the value is built from compile-time constants, with a certain + * tolerance for indirection in order to make this more user-friendly. + * + * This considers a few different things. We accept + * - What TS deems literal (literal strings, literal numbers, literal booleans, + * enum literals), + * - Binary operations of two expressions that we accept (including + * concatenation), + * - Template interpolations of what we accept, + * - `x?y:z` constructions, if we accept `y` and `z` + * - Variables that are const, and initialized with an expression we accept + * + * And to prevent bypasses, expressions that include casts are not accepted. + */ +export function isLiteral(typeChecker: ts.TypeChecker, node: ts.Node): boolean { + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.PlusToken + ) { + // Concatenation is fine, if the parts are literals. + return ( + isLiteral(typeChecker, node.left) && isLiteral(typeChecker, node.right) + ); + } else if (ts.isTemplateExpression(node)) { + // Same for template expressions. + return node.templateSpans.every((span) => { + return isLiteral(typeChecker, span.expression); + }); + } else if (ts.isTemplateLiteral(node)) { + // and literals (in that order). + return true; + } else if (ts.isConditionalExpression(node)) { + return ( + isLiteral(typeChecker, node.whenTrue) && + isLiteral(typeChecker, node.whenFalse) + ); + } else if (ts.isIdentifier(node)) { + return isUnderlyingValueAStringLiteral(node, typeChecker); + } + + const hasCasts = findInChildren( + node, + (n) => ts.isAsExpression(n) && !ts.isConstTypeReference(n.type), + ); + + return !hasCasts && isLiteralAccordingToItsType(typeChecker, node); +} + +/** + * Given an identifier, this function goes around the AST to determine + * whether we should consider it a string literal, on a best-effort basis. It + * is an approximation, but should never have false positives. + */ +function isUnderlyingValueAStringLiteral( + identifier: ts.Identifier, + tc: ts.TypeChecker, +) { + // The identifier references a value, and we try to follow the trail: if we + // find a variable declaration for the identifier, and it was declared as a + // const (so we know it wasn't altered along the way), then the value used + // in the declaration is the value our identifier references. That means we + // should look at the value used in its initialization (by applying the same + // rules as before). + // Since we're best-effort, if a part of that operation failed due to lack + // of support, then we fail closed and don't consider the value a literal. + const declarations = getDeclarations(identifier, tc); + const variableDeclarations = declarations.filter(ts.isVariableDeclaration); + if (variableDeclarations.length) { + return variableDeclarations + .filter(isConst) + .some((d) => d.initializer !== undefined && isLiteral(tc, d.initializer)); + } + const importDeclarations = declarations.filter(ts.isImportSpecifier); + if (importDeclarations.length) { + return isLiteralAccordingToItsType(tc, identifier); + } + return false; +} + +/** + * Returns whether this thing is a literal based on TS's understanding. + */ +function isLiteralAccordingToItsType( + typeChecker: ts.TypeChecker, + node: ts.Node, +): boolean { + const nodeType = typeChecker.getTypeAtLocation(node); + return ( + (nodeType.flags & + (ts.TypeFlags.StringLiteral | + ts.TypeFlags.NumberLiteral | + ts.TypeFlags.BooleanLiteral | + ts.TypeFlags.EnumLiteral)) !== + 0 + ); +} + +/** + * Follows the symbol behind the given identifier, assuming it is a variable, + * and return all the variable declarations we can find that match it in the + * same file. + */ +function getDeclarations( + node: ts.Identifier, + tc: ts.TypeChecker, +): ts.Declaration[] { + const symbol = tc.getSymbolAtLocation(node); + if (!symbol) { + return []; + } + return symbol.getDeclarations() ?? []; +} + +// Tests whether the given variable declaration is Const. +function isConst(varDecl: ts.VariableDeclaration): boolean { + return Boolean( + varDecl && + varDecl.parent && + ts.isVariableDeclarationList(varDecl.parent) && + varDecl.parent.flags & ts.NodeFlags.Const, + ); +} diff --git a/safety-web/src/common/third_party/tsetse/util/is_trusted_type.ts b/safety-web/src/common/third_party/tsetse/util/is_trusted_type.ts new file mode 100644 index 0000000..9d56231 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/is_trusted_type.ts @@ -0,0 +1,136 @@ +import * as ts from 'typescript'; + +import {debugLog} from './ast_tools'; +import {TrustedTypesConfig} from './trusted_types_configuration'; + +/** + * Returns true if the AST expression is Trusted Types compliant and can be + * safely used in the sink. + * + * This function is only called if the rule is configured to allow specific + * Trusted Type value in the assignment. + */ +export function isExpressionOfAllowedTrustedType( + tc: ts.TypeChecker, expr: ts.Expression, + allowedType: TrustedTypesConfig): boolean { + if (isTrustedType(tc, expr, allowedType)) return true; + if (isTrustedTypeCastToUnknownToString(tc, expr, allowedType)) return true; + if (isTrustedTypeUnionWithStringCastToString(tc, expr, allowedType)) return true; + if (isTrustedTypeUnwrapperFunction(tc, expr, allowedType)) return true; + return false; +} + +/** + * Helper function which checks if given TS Symbol is allowed (matches + * configured Trusted Type). + */ +function isAllowedSymbol( + tc: ts.TypeChecker, symbol: ts.Symbol|undefined, + allowedType: TrustedTypesConfig, + allowAmbientTrustedTypesDeclaration: boolean) { + debugLog(() => `isAllowedSymbol called with symbol ${symbol?.getName()}`); + if (!symbol) return false; + + const fqn = tc.getFullyQualifiedName(symbol); + debugLog(() => `fully qualified name is ${fqn}`); + if (allowAmbientTrustedTypesDeclaration && + allowedType.allowAmbientTrustedTypesDeclaration && + fqn === allowedType.typeName) { + return true; + } + + if (!fqn.endsWith('.' + allowedType.typeName)) return false; + + // check that the type is comes allowed declaration file + const declarations = symbol.getDeclarations(); + if (!declarations) return false; + const declarationFileNames = + declarations.map(d => d.getSourceFile()?.fileName); + debugLog(() => `got declaration filenames ${declarationFileNames}`); + + return declarationFileNames.some( + fileName => fileName.includes(allowedType.modulePathMatcher)); +} + +/** + * Returns true if the expression matches the following format: + * "AllowedTrustedType as unknown as string" + */ +function isTrustedTypeCastToUnknownToString( + tc: ts.TypeChecker, expr: ts.Expression, allowedType: TrustedTypesConfig) { + // check if the expression is a cast expression to string + if (!ts.isAsExpression(expr) || + expr.type.kind !== ts.SyntaxKind.StringKeyword) { + return false; + } + + // inner expression should be another cast expression + const innerExpr = expr.expression; + if (!ts.isAsExpression(innerExpr) || + innerExpr.type.kind !== ts.SyntaxKind.UnknownKeyword) { + return false; + } + + // check if left side of the cast expression is of an allowed type. + const castSource = innerExpr.expression; + debugLog(() => `looking at cast source ${castSource.getText()}`); + return isAllowedSymbol( + tc, tc.getTypeAtLocation(castSource).getSymbol(), allowedType, false); +} + +/** + * Returns true if the expression matches the following format: + * "(AllowedTrustedType | string) as string" + */ +function isTrustedTypeUnionWithStringCastToString( + tc: ts.TypeChecker, expr: ts.Expression, allowedType: TrustedTypesConfig) { + // verify that the expression is a cast expression to string + if (!ts.isAsExpression(expr) || + expr.type.kind !== ts.SyntaxKind.StringKeyword) { + return false; + } + + // inner expression needs to be a type union of trusted value + const innerExprType = tc.getTypeAtLocation(expr.expression); + return innerExprType.isUnion() && + // do not care how many types are in the union. As long as one of them is + // the configured Trusted type we are happy. + innerExprType.types.some( + type => isAllowedSymbol(tc, type.getSymbol(), allowedType, false)); +} + +/** + * Returns true if the expression is a function call of the following signature: + * "(TypeCompatibleWithTrustedType): string" + * + * where `TypeCompatibleWithTrustedType` can be either the Trusted Type itself + * or a TS union. We only require the first argument of the call site to be + * exact Trusted Type. Pattern like `unwrap('err msg', TT)` will not work. + * We intentionally want make the unwrapper pattern more apparent by forcing the + * TT value in the first argument. + */ +function isTrustedTypeUnwrapperFunction( + tc: ts.TypeChecker, expr: ts.Expression, allowedType: TrustedTypesConfig) { + if (!ts.isCallExpression(expr)) return false; + + return expr.arguments.length > 0 && + isAllowedSymbol( + tc, tc.getTypeAtLocation(expr.arguments[0]).getSymbol(), + allowedType, false); +} + +/** + * Returns true if the expression is a value of Trusted Types, or a type that is + * the intersection of Trusted Types and other types. + */ +function isTrustedType( + tc: ts.TypeChecker, expr: ts.Expression, allowedType: TrustedTypesConfig) { + const type = tc.getTypeAtLocation(expr); + + if (isAllowedSymbol(tc, type.getSymbol(), allowedType, true)) return true; + + if (!type.isIntersection()) return false; + + return type.types.some( + t => isAllowedSymbol(tc, t.getSymbol(), allowedType, true)); +} diff --git a/safety-web/src/common/third_party/tsetse/util/pattern_config.ts b/safety-web/src/common/third_party/tsetse/util/pattern_config.ts new file mode 100644 index 0000000..9f25e56 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/pattern_config.ts @@ -0,0 +1,75 @@ +import {AllowlistEntry} from '../allowlist'; +import {TrustedTypesConfig} from './trusted_types_configuration'; + +/** + * The list of supported patterns useable in ConformancePatternRule. The + * patterns whose name match JSConformance patterns should behave similarly (see + * https://github.com/google/closure-compiler/wiki/JS-Conformance-Framework) + */ +export enum PatternKind { + /** Ban use of fully distinguished names. */ + BANNED_NAME = 'banned-name', + /** Ban use of fully distinguished names, even in import statements. */ + BANNED_IMPORTED_NAME = 'banned-imported-name', + /** Ban use of instance properties */ + BANNED_PROPERTY = 'banned-property', + /** + * Ban instance property, like BANNED_PROPERTY but where reads of the + * property are allowed. + */ + BANNED_PROPERTY_WRITE = 'banned-property-write', + /** + * Ban instance property write unless the property is assigned a constant + * literal. + */ + BANNED_PROPERTY_NON_CONSTANT_WRITE = 'banned-property-non-constant-write', +} + +/** + * A config for `PatternEngine`. + */ +export interface PatternEngineConfig { + /** + * Values have a pattern-specific syntax. See each patternKind's tests for + * examples. + */ + values: string[]; + + /** The error code assigned to this pattern. */ + errorCode: number; + + /** The error message this pattern will create. */ + errorMessage: string; + + /** A list of allowlist blocks. */ + allowlistEntries?: AllowlistEntry[]; + + /** + * Type of the allowed Trusted value by the rule or custom + * `TrustedTypesConfig`. + */ + allowedTrustedType?: TrustedTypesConfig; +} + +/** + * A config for `ConformancePatternRule`. + */ +export interface PatternRuleConfig extends PatternEngineConfig { + kind: PatternKind; + + /** + * An optional name for that rule, which will be the rule's `ruleName`. + * Should be lower-dashed-case. + */ + name?: string; +} + +/** + * Internal function to override the rule config properties before passing to + * parent constructor. + */ +export function overridePatternConfig(config: PatternRuleConfig): + PatternRuleConfig { + + return config; +} diff --git a/safety-web/src/common/third_party/tsetse/util/pattern_engines/name_engine.ts b/safety-web/src/common/third_party/tsetse/util/pattern_engines/name_engine.ts new file mode 100644 index 0000000..9d1595d --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/pattern_engines/name_engine.ts @@ -0,0 +1,95 @@ +import * as ts from 'typescript'; + +import {Checker} from '../../checker'; +import {AbsoluteMatcher} from '../absolute_matcher'; +import {isExpressionOfAllowedTrustedType} from '../is_trusted_type'; +import {TrustedTypesConfig} from '../trusted_types_configuration'; + +import {PatternEngine} from './pattern_engine'; + +function isCalledWithAllowedTrustedType( + tc: ts.TypeChecker, n: ts.Node, + allowedTrustedType: TrustedTypesConfig|undefined) { + const par = n.parent; + if (allowedTrustedType && ts.isCallExpression(par) && + par.arguments.length > 0 && + isExpressionOfAllowedTrustedType( + tc, par.arguments[0], allowedTrustedType)) { + return true; + } + + return false; +} + +function isPolyfill(n: ts.Node, matcher: AbsoluteMatcher) { + if (matcher.filePath === 'GLOBAL') { + const parent = n.parent; + if (parent && ts.isBinaryExpression(parent) && parent.left === n && + parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + return true; + } + } + return false; +} + +function checkIdentifierNode( + tc: ts.TypeChecker, n: ts.Identifier, matcher: AbsoluteMatcher, + allowedTrustedType: TrustedTypesConfig|undefined): ts.Node|undefined { + const wholeExp = ts.isPropertyAccessExpression(n.parent) ? n.parent : n; + if (isPolyfill(wholeExp, matcher)) return; + if (!matcher.matches(n, tc)) return; + if (isCalledWithAllowedTrustedType(tc, n, allowedTrustedType)) return; + + return wholeExp; +} + +function checkElementAccessNode( + tc: ts.TypeChecker, n: ts.ElementAccessExpression, matcher: AbsoluteMatcher, + allowedTrustedType: TrustedTypesConfig|undefined): ts.Node|undefined { + if (isPolyfill(n, matcher)) return; + if (!matcher.matches(n.argumentExpression, tc)) return; + if (isCalledWithAllowedTrustedType(tc, n, allowedTrustedType)) return; + + return n; +} + +/** Engine for the BANNED_NAME pattern */ +export class NameEngine extends PatternEngine { + protected readonly banImport: boolean = false; + + register(checker: Checker) { + for (const value of this.config.values) { + const matcher = new AbsoluteMatcher(value, this.banImport); + + // `String.prototype.split` only returns emtpy array when both the + // string and the splitter are empty. Here we should be able to safely + // assert pop returns a non-null result. + const bannedIdName = matcher.bannedName.split('.').pop()!; + checker.onNamedIdentifier( + bannedIdName, + this.wrapCheckWithAllowlistingAndFixer( + (tc, n) => checkIdentifierNode( + tc, n, matcher, this.config.allowedTrustedType)), + this.config.errorCode); + + // `checker.onNamedIdentifier` will not match the node if it is accessed + // using property access expression (e.g. window['eval']). + // We already found examples on how developers misuse this limitation + // internally. + // + // This engine is inteded to ban global name identifiers, but even these + // can be property accessed using `globalThis` or `window`. + checker.onStringLiteralElementAccess( + bannedIdName, + this.wrapCheckWithAllowlistingAndFixer( + (tc, n: ts.ElementAccessExpression) => checkElementAccessNode( + tc, n, matcher, this.config.allowedTrustedType)), + this.config.errorCode); + } + } +} + +/** Engine for the BANNED_IMPORTED_NAME pattern */ +export class ImportedNameEngine extends NameEngine { + protected override readonly banImport: boolean = true; +} diff --git a/safety-web/src/common/third_party/tsetse/util/pattern_engines/pattern_engine.ts b/safety-web/src/common/third_party/tsetse/util/pattern_engines/pattern_engine.ts new file mode 100644 index 0000000..44b1091 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/pattern_engines/pattern_engine.ts @@ -0,0 +1,54 @@ +import * as ts from 'typescript'; + +import {Allowlist} from '../../allowlist'; +import {Checker} from '../../checker'; +import {Fix, Fixer} from '../../util/fixer'; +import {PatternEngineConfig} from '../../util/pattern_config'; +import {shouldExamineNode} from '../ast_tools'; + +/** + * A patternEngine is the logic that handles a specific PatternKind. + */ +export abstract class PatternEngine { + private readonly allowlist: Allowlist; + + constructor( + protected readonly ruleName: string, + protected readonly config: PatternEngineConfig, + protected readonly fixers?: Fixer[]) { + this.allowlist = new Allowlist(config.allowlistEntries); + } + + /** + * `register` will be called by the ConformanceRule to tell Tsetse the + * PatternEngine will handle matching. Implementations should use + * `checkAndFilterResults` as a wrapper for `check`. + */ + abstract register(checker: Checker): void; + + /** + * A composer that wraps checking functions with code handling aspects of the + * analysis that are not engine-specific, and which defers to the + * subclass-specific logic afterwards. Subclasses should transform their + * checking logic with this composer before registered on the checker. + */ + protected wrapCheckWithAllowlistingAndFixer( + checkFunction: (tc: ts.TypeChecker, n: T) => ts.Node | + undefined): (c: Checker, n: T) => void { + return (c: Checker, n: T) => { + const sf = n.getSourceFile(); + if (!shouldExamineNode(n) || sf.isDeclarationFile) { + return; + } + const matchedNode = checkFunction(c.typeChecker, n); + if (matchedNode) { + const fixes = + this.fixers?.map(fixer => fixer.getFixForFlaggedNode(matchedNode)) + ?.filter((fix): fix is Fix => fix !== undefined); + c.addFailureAtNode( + matchedNode, this.config.errorMessage, this.ruleName, + this.allowlist, fixes); + } + }; + } +} diff --git a/safety-web/src/common/third_party/tsetse/util/pattern_engines/property_engine.ts b/safety-web/src/common/third_party/tsetse/util/pattern_engines/property_engine.ts new file mode 100644 index 0000000..6506261 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/pattern_engines/property_engine.ts @@ -0,0 +1,44 @@ +import * as ts from 'typescript'; + +import {Checker} from '../../checker'; +import {debugLog} from '../ast_tools'; +import {PropertyMatcher} from '../property_matcher'; + +import {PatternEngine} from './pattern_engine'; + +/** Match an AST node with a property matcher. */ +export function matchProperty( + tc: ts.TypeChecker, + n: ts.PropertyAccessExpression|ts.ElementAccessExpression, + matcher: PropertyMatcher): ts.Node|undefined { + debugLog(() => `inspecting ${n.getText().trim()}`); + if (!matcher.typeMatches(tc.getTypeAtLocation(n.expression))) return; + return n; +} + +/** + * Engine for the BANNED_PROPERTY pattern. It captures accesses to property + * matching the spec regardless whether it's a read or write. + */ +export class PropertyEngine extends PatternEngine { + protected registerWith(checker: Checker, matchNode: typeof matchProperty) { + for (const value of this.config.values) { + const matcher = PropertyMatcher.fromSpec(value); + checker.onNamedPropertyAccess( + matcher.bannedProperty, + this.wrapCheckWithAllowlistingAndFixer( + (tc, n) => matchNode(tc, n, matcher)), + this.config.errorCode); + + checker.onStringLiteralElementAccess( + matcher.bannedProperty, + this.wrapCheckWithAllowlistingAndFixer( + (tc, n) => matchNode(tc, n, matcher)), + this.config.errorCode); + } + } + + register(checker: Checker) { + this.registerWith(checker, matchProperty); + } +} diff --git a/safety-web/src/common/third_party/tsetse/util/pattern_engines/property_non_constant_write_engine.ts b/safety-web/src/common/third_party/tsetse/util/pattern_engines/property_non_constant_write_engine.ts new file mode 100644 index 0000000..2d0b5c2 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/pattern_engines/property_non_constant_write_engine.ts @@ -0,0 +1,35 @@ +import * as ts from 'typescript'; + +import {Checker} from '../../checker'; +import {debugLog} from '../ast_tools'; +import {isLiteral} from '../is_literal'; +import {PropertyMatcher} from '../property_matcher'; + +import {matchPropertyWrite, PropertyWriteEngine} from './property_write_engine'; + +function matchPropertyNonConstantWrite( + tc: ts.TypeChecker, + n: ts.PropertyAccessExpression|ts.ElementAccessExpression, + matcher: PropertyMatcher): ts.Node|undefined { + debugLog(() => `inspecting ${n.getFullText().trim()}`); + if (matchPropertyWrite(tc, n, matcher) === undefined) { + return; + } + const rval = (n.parent as ts.BinaryExpression).right; + if (isLiteral(tc, rval)) { + debugLog( + () => `Assigned value (${ + rval.getFullText()}) is a compile-time constant.`); + return; + } + return n.parent; +} + +/** + * The engine for BANNED_PROPERTY_NON_CONSTANT_WRITE. + */ +export class PropertyNonConstantWriteEngine extends PropertyWriteEngine { + override register(checker: Checker) { + this.registerWith(checker, matchPropertyNonConstantWrite); + } +} diff --git a/safety-web/src/common/third_party/tsetse/util/pattern_engines/property_write_engine.ts b/safety-web/src/common/third_party/tsetse/util/pattern_engines/property_write_engine.ts new file mode 100644 index 0000000..f51a8b2 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/pattern_engines/property_write_engine.ts @@ -0,0 +1,69 @@ +import * as ts from 'typescript'; + +import {Checker} from '../../checker'; +import {debugLog} from '../ast_tools'; +import {isExpressionOfAllowedTrustedType} from '../is_trusted_type'; +import {PropertyMatcher} from '../property_matcher'; +import {TrustedTypesConfig} from '../trusted_types_configuration'; + +import {matchProperty, PropertyEngine} from './property_engine'; + +/** Test if an AST node is a matched property write. */ +export function matchPropertyWrite( + tc: ts.TypeChecker, + n: ts.PropertyAccessExpression|ts.ElementAccessExpression, + matcher: PropertyMatcher): ts.BinaryExpression|undefined { + debugLog(() => `inspecting ${n.parent.getText().trim()}`); + + if (matchProperty(tc, n, matcher) === undefined) return; + + const assignment = n.parent; + + if (!ts.isBinaryExpression(assignment)) return; + // All properties we track are of the string type, so we only look at + // `=` and `+=` operators. + if (assignment.operatorToken.kind !== ts.SyntaxKind.EqualsToken && + assignment.operatorToken.kind !== ts.SyntaxKind.PlusEqualsToken) { + return; + } + if (assignment.left !== n) return; + + return assignment; +} + +/** + * Checks whether the access expression is part of an assignment (write) to the + * matched property and the type of the right hand side value is not of the + * an allowed type. + * + * Returns `undefined` if the property write is allowed and the assignment node + * if the assignment should trigger violation. + */ +function allowTrustedExpressionOnMatchedProperty( + allowedType: TrustedTypesConfig|undefined, tc: ts.TypeChecker, + n: ts.PropertyAccessExpression|ts.ElementAccessExpression, + matcher: PropertyMatcher): ts.BinaryExpression|undefined { + const assignment = matchPropertyWrite(tc, n, matcher); + if (!assignment) return; + + if (allowedType && + isExpressionOfAllowedTrustedType(tc, assignment.right, allowedType)) { + return; + } + + return assignment; +} + +/** + * The engine for BANNED_PROPERTY_WRITE. Bans assignments to the restricted + * properties unless the right hand side of the assignment is of an allowed + * type. + */ +export class PropertyWriteEngine extends PropertyEngine { + override register(checker: Checker) { + this.registerWith( + checker, + (tc, n, m) => allowTrustedExpressionOnMatchedProperty( + this.config.allowedTrustedType, tc, n, m)); + } +} diff --git a/safety-web/src/common/third_party/tsetse/util/property_matcher.ts b/safety-web/src/common/third_party/tsetse/util/property_matcher.ts new file mode 100644 index 0000000..2b15bd7 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/property_matcher.ts @@ -0,0 +1,62 @@ +import * as ts from 'typescript'; + +/** + * This class matches a property access node, based on a property holder type + * (through its name), i.e. a class, and a property name. + * + * The logic is voluntarily simple: if a matcher for `a.b` tests a `x.y` node, + * it will return true if: + * - `x` is of type `a` either directly (name-based) or through inheritance + * (ditto), + * - and, textually, `y` === `b`. + * + * Note that the logic is different from TS's type system: this matcher doesn't + * have any knowledge of structural typing. + */ +export class PropertyMatcher { + static fromSpec(spec: string): PropertyMatcher { + if (spec.indexOf('.prototype.') === -1) { + throw new Error(`BANNED_PROPERTY expects a .prototype in your query.`); + } + const requestParser = /^([\w\d_.-]+)\.prototype\.([\w\d_.-]+)$/; + const matches = requestParser.exec(spec); + if (!matches) { + throw new Error('Cannot understand the BannedProperty spec' + spec); + } + const [bannedType, bannedProperty] = matches.slice(1); + return new PropertyMatcher(bannedType, bannedProperty); + } + + constructor(readonly bannedType: string, readonly bannedProperty: string) {} + + /** + * @param n The PropertyAccessExpression we're looking at. + */ + matches(n: ts.PropertyAccessExpression, tc: ts.TypeChecker) { + return n.name.text === this.bannedProperty && + this.typeMatches(tc.getTypeAtLocation(n.expression)); + } + + /** + * Match types recursively in the lattice. This function over-approximates + * the result by considering union types and intersection types as the same. + */ + typeMatches(inspectedType: ts.Type): boolean { + // Skip checking mocked objects + if (inspectedType.aliasSymbol?.escapedName === 'SpyObj') return false; + + // Exact type match + if (inspectedType.getSymbol()?.getName() === this.bannedType) { + return true; + } + + // If the type is an intersection/union, check if any of the component + // matches + if (inspectedType.isUnionOrIntersection()) { + return inspectedType.types.some(comp => this.typeMatches(comp)); + } + + const baseTypes = inspectedType.getBaseTypes() || []; + return baseTypes.some(base => this.typeMatches(base)); + } +} \ No newline at end of file diff --git a/safety-web/src/common/third_party/tsetse/util/trusted_types_configuration.ts b/safety-web/src/common/third_party/tsetse/util/trusted_types_configuration.ts new file mode 100644 index 0000000..fa1b302 --- /dev/null +++ b/safety-web/src/common/third_party/tsetse/util/trusted_types_configuration.ts @@ -0,0 +1,52 @@ +/** Names of all Trusted Types */ +export type TrustedTypes = 'TrustedHTML'|'TrustedScript'|'TrustedScriptURL'; +/** + * Trusted Types configuration used to match Trusted values in the assignments + * to sinks. + */ +export interface TrustedTypesConfig { + allowAmbientTrustedTypesDeclaration: boolean; + /** + * A characteristic component of the absolute path of the definition file. + */ + modulePathMatcher: string; + /** + * The fully qualified name of the trusted type to allow. E.g. + * "global.TrustedHTML". + */ + typeName: TrustedTypes; +} + +/** + * Create `TrustedTypesConfig` for the given Trusted Type. + */ +function createDefaultTrustedTypeConfig(type: TrustedTypes): + TrustedTypesConfig { + const config = { + allowAmbientTrustedTypesDeclaration: true, + // the module path may look like + // "/home/username/.../node_modules/@types/trusted-types/" + modulePathMatcher: '/node_modules/@types/trusted-types/', + typeName: type, + }; + + return config; +} + +/** + * Trusted Types configuration allowing usage of `TrustedHTML` for a given rule. + */ +export const TRUSTED_HTML = createDefaultTrustedTypeConfig('TrustedHTML'); + +/** + * Trusted Types configuration allowing usage of `TrustedScript` for a given + * rule. + */ +export const TRUSTED_SCRIPT = createDefaultTrustedTypeConfig('TrustedScript'); + +/** + * Trusted Types configuration allowing usage of `TrustedScriptURL` for a given + * rule. + */ +export const TRUSTED_SCRIPT_URL = + createDefaultTrustedTypeConfig('TrustedScriptURL'); diff --git a/safety-web/src/common/tsconfig.json b/safety-web/src/common/tsconfig.json new file mode 100644 index 0000000..fc29337 --- /dev/null +++ b/safety-web/src/common/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig-compile.json", + "compilerOptions": { + "composite": true, + "types": ["node", "glob"], + "outDir": "lib" + }, + "include": ["*.ts", "rules", "third_party"] +} diff --git a/safety-web/src/index.ts b/safety-web/src/index.ts new file mode 100644 index 0000000..8ba8088 --- /dev/null +++ b/safety-web/src/index.ts @@ -0,0 +1,23 @@ +// Copyright 2024 Google LLC +// +// 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 +// +// https://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 { trustedTypesChecks } from './trusted_types_checks'; + +/** + * Exports 'trusted-types-checks' rule so that those using the ESLint plugin + * can access it + */ +export const rules = { + 'trusted-types-checks': trustedTypesChecks, +}; diff --git a/safety-web/src/trusted_types_checks.ts b/safety-web/src/trusted_types_checks.ts new file mode 100644 index 0000000..d4477be --- /dev/null +++ b/safety-web/src/trusted_types_checks.ts @@ -0,0 +1,95 @@ +// Copyright 2024 Google LLC +// +// 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 +// +// https://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 { ESLintUtils } from '@typescript-eslint/utils'; +import { getConfiguredChecker } from './common/configured_checker'; +import { Checker } from './common/third_party/tsetse/checker'; +import * as ts from 'typescript'; +import { tsetseMessageToMessageId, messageIdMap } from './tsetse_compat'; + +const createRule = ESLintUtils.RuleCreator( + () => 'safety-web', +); + +// Cached checker instantiated only once. +let checker: Checker; + +/** +* Rule to check Trusted Types compliance +*/ +export const trustedTypesChecks = createRule({ + name: 'trusted-types-checks', + meta: { + type: 'problem', + docs: { + description: 'Checks Trusted Types compliance', + recommended: 'strict', + }, + messages: { + ...messageIdMap, + unknown_rule_triggered: 'trusted-types-checks reported a violation that could not be mapped to a known violation id.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + // Skip checking declaration files + if (context.filename.endsWith('.d.ts')) { + return {}; + } + + const parserServices = ESLintUtils.getParserServices(context); + if (!checker) { + checker = getConfiguredChecker( + parserServices.program, + ts.createCompilerHost(parserServices.program.getCompilerOptions()), + ).checker; + } + return { + Program(node) { + const parserServices = ESLintUtils.getParserServices(context); + const rootNode = parserServices.esTreeNodeToTSNodeMap.get(node); + + // Run all enabled checks + const { failures } = checker.execute(rootNode, true); + + // Report the detected errors + for (const failure of failures) { + const diagnostic = failure.toDiagnostic(); + const start = ts.getLineAndCharacterOfPosition( + rootNode, + diagnostic.start, + ); + const end = ts.getLineAndCharacterOfPosition( + rootNode, + diagnostic.end, + ); + + context.report({ + loc: { + start: { line: start.line + 1, column: start.character }, + end: { line: end.line + 1, column: end.character }, + }, + messageId: tsetseMessageToMessageId( + // TODO: refine `toDiagnostic` to refine type and remove this cast. + diagnostic.messageText as string) || 'unknown_rule_triggered', + data: { + tsetseMessage: diagnostic.messageText + } + }); + } + }, + }; + } +}); diff --git a/safety-web/src/tsetse_compat.ts b/safety-web/src/tsetse_compat.ts new file mode 100644 index 0000000..d2cec46 --- /dev/null +++ b/safety-web/src/tsetse_compat.ts @@ -0,0 +1,91 @@ +import { Rule as banBaseHrefAssignments } from './common/rules/dom_security/ban_base_href_assignments'; +import { Rule as banDocumentExeccommand } from './common/rules/dom_security/ban_document_execcommand'; +import { Rule as banDocumentWriteCalls } from './common/rules/dom_security/ban_document_write_calls'; +import { Rule as banDocumentWritelnCalls } from './common/rules/dom_security/ban_document_writeln_calls'; +import { Rule as banDomparserParsefromstring } from './common/rules/dom_security/ban_domparser_parsefromstring'; +import { Rule as banElementInnerhtmlAssignments } from './common/rules/dom_security/ban_element_innerhtml_assignments'; +import { Rule as banElementInsertadjacenthtml } from './common/rules/dom_security/ban_element_insertadjacenthtml'; +import { Rule as banElementOuterhtmlAssignments } from './common/rules/dom_security/ban_element_outerhtml_assignments'; +import { Rule as banElementSetattribute } from './common/rules/dom_security/ban_element_setattribute'; +import { Rule as banEvalCalls } from './common/rules/dom_security/ban_eval_calls'; +import { Rule as banFunctionCalls } from './common/rules/dom_security/ban_function_calls'; +import { Rule as banIframeSrcdocAssignments } from './common/rules/dom_security/ban_iframe_srcdoc_assignments'; +import { Rule as banObjectDataAssignments } from './common/rules/dom_security/ban_object_data_assignments'; +import { Rule as banRangeCreatecontextualfragment } from './common/rules/dom_security/ban_range_createcontextualfragment'; +import { Rule as banScriptAppendchildCalls } from './common/rules/dom_security/ban_script_appendchild_calls'; +import { Rule as banScriptContentAssignments } from './common/rules/dom_security/ban_script_content_assignments'; +import { Rule as banScriptSrcAssignments } from './common/rules/dom_security/ban_script_src_assignments'; +import { Rule as banServiceworkercontainerRegister } from './common/rules/dom_security/ban_serviceworkercontainer_register'; +import { Rule as banSharedWorkerCalls } from './common/rules/dom_security/ban_shared_worker_calls'; +import { Rule as banTrustedtypesCreatepolicy } from './common/rules/dom_security/ban_trustedtypes_createpolicy'; +import { Rule as banWindowStringfunctiondef } from './common/rules/dom_security/ban_window_stringfunctiondef'; +import { Rule as banWorkerCalls } from './common/rules/dom_security/ban_worker_calls'; +import { Rule as banWorkerImportscripts } from './common/rules/dom_security/ban_worker_importscripts'; +import { Rule as banLegacyConversions } from './common/rules/unsafe/ban_legacy_conversions'; +import { Rule as banReviewedConversions } from './common/rules/unsafe/ban_reviewed_conversions'; + +export const messageIdMap = { + ban_base_href_assignments: '{{ tsetseMessage }}', + ban_document_execcommand: '{{ tsetseMessage }}', + ban_document_write_calls: '{{ tsetseMessage }}', + ban_document_writeln_calls: '{{ tsetseMessage }}', + ban_domparser_parsefromstring: '{{ tsetseMessage }}', + ban_element_innerhtml_assignments: '{{ tsetseMessage }}', + ban_element_insertadjacenthtml: '{{ tsetseMessage }}', + ban_element_outerhtml_assignments: '{{ tsetseMessage }}', + ban_element_setattribute: '{{ tsetseMessage }}', + ban_eval_calls: '{{ tsetseMessage }}', + ban_function_calls: '{{ tsetseMessage }}', + ban_iframe_srcdoc_assignments: '{{ tsetseMessage }}', + ban_object_data_assignments: '{{ tsetseMessage }}', + ban_range_createcontextualfragment: '{{ tsetseMessage }}', + ban_script_appendchild_calls: '{{ tsetseMessage }}', + ban_script_content_assignments: '{{ tsetseMessage }}', + ban_script_src_assignments: '{{ tsetseMessage }}', + ban_serviceworkercontainer_register: '{{ tsetseMessage }}', + ban_shared_worker_calls: '{{ tsetseMessage }}', + ban_trustedtypes_createpolicy: '{{ tsetseMessage }}', + ban_window_stringfunctiondef: '{{ tsetseMessage }}', + ban_worker_calls: '{{ tsetseMessage }}', + ban_worker_importscripts: '{{ tsetseMessage }}', + ban_legacy_conversions: '{{ tsetseMessage }}', + ban_reviewed_conversions: '{{ tsetseMessage }}', +}; + +export type TrustedTypeCheckMessageId = keyof typeof messageIdMap; + +const ruleNameToMessageIdMap: Map = new Map([ + [banBaseHrefAssignments.RULE_NAME, 'ban_base_href_assignments'], + [banDocumentExeccommand.RULE_NAME, 'ban_document_execcommand'], + [banDocumentWriteCalls.RULE_NAME, 'ban_document_write_calls'], + [banDocumentWritelnCalls.RULE_NAME, 'ban_document_writeln_calls'], + [banDomparserParsefromstring.RULE_NAME, 'ban_domparser_parsefromstring'], + [banElementInnerhtmlAssignments.RULE_NAME, 'ban_element_innerhtml_assignments'], + [banElementInsertadjacenthtml.RULE_NAME, 'ban_element_insertadjacenthtml'], + [banElementOuterhtmlAssignments.RULE_NAME, 'ban_element_outerhtml_assignments'], + [banElementSetattribute.RULE_NAME, 'ban_element_setattribute'], + [banEvalCalls.RULE_NAME, 'ban_eval_calls'], + [banFunctionCalls.RULE_NAME, 'ban_function_calls'], + [banIframeSrcdocAssignments.RULE_NAME, 'ban_iframe_srcdoc_assignments'], + [banObjectDataAssignments.RULE_NAME, 'ban_object_data_assignments'], + [banRangeCreatecontextualfragment.RULE_NAME, 'ban_range_createcontextualfragment'], + [banScriptAppendchildCalls.RULE_NAME, 'ban_script_appendchild_calls'], + [banScriptContentAssignments.RULE_NAME, 'ban_script_content_assignments'], + [banScriptSrcAssignments.RULE_NAME, 'ban_script_src_assignments'], + [banServiceworkercontainerRegister.RULE_NAME, 'ban_serviceworkercontainer_register'], + [banSharedWorkerCalls.RULE_NAME, 'ban_shared_worker_calls'], + [banTrustedtypesCreatepolicy.RULE_NAME, 'ban_trustedtypes_createpolicy'], + [banWindowStringfunctiondef.RULE_NAME, 'ban_window_stringfunctiondef'], + [banWorkerCalls.RULE_NAME, 'ban_worker_calls'], + [banWorkerImportscripts.RULE_NAME, 'ban_worker_importscripts'], + [banLegacyConversions.RULE_NAME, 'ban_legacy_conversions'], + [banReviewedConversions.RULE_NAME, 'ban_reviewed_conversions'], +]); + +export function tsetseMessageToMessageId(tsetseMessage: string): TrustedTypeCheckMessageId | undefined { + const match = tsetseMessage.match(/^\[([a-z-]+)\]/); + if (match !== null) { + return ruleNameToMessageIdMap.get(match[1]); + } + return undefined; +} diff --git a/safety-web/test/test.ts b/safety-web/test/test.ts new file mode 100644 index 0000000..17c946e --- /dev/null +++ b/safety-web/test/test.ts @@ -0,0 +1,47 @@ +// Copyright 2024 Google LLC +// +// 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 +// +// https://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 { RuleTester } from '@typescript-eslint/rule-tester'; +import * as mocha from 'mocha'; +import { trustedTypesChecks } from '../src/trusted_types_checks'; + +RuleTester.afterAll = mocha.after; +const ruleTester = new RuleTester( + { + parser: '@typescript-eslint/parser', + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname + "/test_fixtures", + // Required for the DOM APIs to typecheck correctly. + lib: ['dom'], + }, + } +); + +ruleTester.run('trusted-types-checks', trustedTypesChecks, + { + valid: [ + 'const x = 1;', + ], + invalid: [ + { + code: `document.createElement('script').innerHTML = 'foo';`, + errors: [ + { + messageId: 'ban_element_innerhtml_assignments', + }, + ], + }, + ], + }); diff --git a/safety-web/test/test_fixtures/file.ts b/safety-web/test/test_fixtures/file.ts new file mode 100644 index 0000000..e69de29 diff --git a/safety-web/test/test_fixtures/react.tsx b/safety-web/test/test_fixtures/react.tsx new file mode 100644 index 0000000..e69de29 diff --git a/safety-web/test/test_fixtures/tsconfig.json b/safety-web/test/test_fixtures/tsconfig.json new file mode 100644 index 0000000..28334f3 --- /dev/null +++ b/safety-web/test/test_fixtures/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "strict": true, + }, + "include": [ + "file.ts", + "react.tsx" + ] +} diff --git a/safety-web/tsconfig.json b/safety-web/tsconfig.json new file mode 100644 index 0000000..cf81efd --- /dev/null +++ b/safety-web/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "nodenext", + "target": "ES2015", + "outDir": "lib" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts", + ], + } + \ No newline at end of file diff --git a/safety-web/update_tsetse_logs.txt b/safety-web/update_tsetse_logs.txt new file mode 100644 index 0000000..0193971 --- /dev/null +++ b/safety-web/update_tsetse_logs.txt @@ -0,0 +1,11 @@ +Most recent run of update_tsetse.sh: Fri Jul 5 08:54:26 AM UTC 2024 +--- +Most recent tsetse commit (from https://github.com/google/tsec.git): +commit bc0317c711b2d9afe008b4ecb21e85759232a7f0 +Author: ISE Hardening +Date: Tue Jun 4 11:04:23 2024 -0700 + + No public description + + PiperOrigin-RevId: 640214558 + Change-Id: I67a9523e9bd8e3d98a6c7f296e281ffc62009adb diff --git a/test-helpers/expect-violations/README.md b/test-helpers/expect-violations/README.md new file mode 100644 index 0000000..290c534 --- /dev/null +++ b/test-helpers/expect-violations/README.md @@ -0,0 +1,7 @@ +# NPM binary to check for expected violations in integration tests + +Usage: + +```bash +yarn --silent eslint --format json | npx expected-violations +``` diff --git a/test-helpers/expect-violations/package.json b/test-helpers/expect-violations/package.json new file mode 100644 index 0000000..041199e --- /dev/null +++ b/test-helpers/expect-violations/package.json @@ -0,0 +1,20 @@ +{ + "name": "expect-violations", + "version": "0.0.1", + "private": true, + "type": "module", + "dependencies": { + "chai": "^5.1.1", + "yargs": "^17.7.2" + }, + "bin": "./bin/index.js", + "scripts": { + "clean": "tsc --build --clean", + "build": "tsc && chmod +x bin/index.js", + "lint": "echo TODO", + "test": "echo TODO" + }, + "devDependencies": { + "@types/yargs": "^17.0.32" + } +} diff --git a/test-helpers/expect-violations/src/index.ts b/test-helpers/expect-violations/src/index.ts new file mode 100644 index 0000000..c2c221a --- /dev/null +++ b/test-helpers/expect-violations/src/index.ts @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +// Copyright 2024 Google LLC +// +// 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 +// +// https://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 { expect } from 'chai'; +import fs from 'node:fs'; +import type { ViolationReport } from './violations.js'; +import * as path from 'path'; +import yargs from 'yargs'; + +function main() { + const options = yargs(process.argv.slice(2)) + .scriptName('expect-violations') + .command('update ', 'Updates the expected violation file') + .command('test ', 'Test the expected violation file') + .demandCommand() + .parseSync(); + + const command = options._[0] as 'update' | 'test'; + const expectatedViolationReportPath = options.expected_file_json as string; + + const expectedViolationReport = getExpectedViolations(expectatedViolationReportPath) as ViolationReport; + const actualESLintReport = JSON.parse(fs.readFileSync(0).toString()) as ViolationReport; // STDIN_FILENO = 0 + const canonicalizedReport = canonicalizeViolationReport(actualESLintReport); + switch (command) { + case 'test': + test(canonicalizedReport, expectedViolationReport); + break; + case 'update': + update(canonicalizedReport, expectedViolationReport, expectatedViolationReportPath); + break; + } +} + +function test(canonicalizedReport: ViolationReport, expectedViolationReport: ViolationReport) { + expect(canonicalizedReport).to.deep.equal(expectedViolationReport); +} + +function update(canonicalizedReport: ViolationReport, expectedViolationReport: ViolationReport, filePath: string) { + let isEqual = true; + try { + expect(JSON.parse(JSON.stringify(canonicalizedReport, null, 2))).to.deep.equal(expectedViolationReport); + } catch { + isEqual = false; + } + if (isEqual) { + console.log('Expected violations already up to date.'); + } else { + fs.writeFileSync(filePath, JSON.stringify(canonicalizedReport, null, 2)); + console.log(`Updated the violation file: ${filePath}`); + } +} + +function canonicalizeViolationReport(report: ViolationReport): ViolationReport { + const canonicalizedReport = []; + for (const fileEntry of report) { + if (fileEntry.messages.length !== 0) { + canonicalizedReport.push({ + filePath: canonicalizePath(fileEntry.filePath), + messages: fileEntry.messages.map(({ ruleId, message, line, column, messageId, endLine, endColumn, }) => ({ ruleId, message, line, column, messageId, endLine, endColumn, })) + }); + } + } + return canonicalizedReport; +} + +function canonicalizePath(filePath: string): string { + // This is not enough to identity the file, but it's good enough in the test to compare ouputs. + return `[...]/${path.basename(filePath)}`; +} + +function getExpectedViolations(path: string) { + try { + const jsonViolations = fs.readFileSync(path, 'utf8'); + return JSON.parse(jsonViolations); + } catch (err) { + console.error(err); + } +} + +main(); diff --git a/test-helpers/expect-violations/src/violations.ts b/test-helpers/expect-violations/src/violations.ts new file mode 100644 index 0000000..bd5ce38 --- /dev/null +++ b/test-helpers/expect-violations/src/violations.ts @@ -0,0 +1,29 @@ +// Copyright 2024 Google LLC +// +// 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 +// +// https://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. + +/** Simplified interface for the JSON array returned by ESLint with the findings. */ +export type ViolationReport = Array; + +interface FileViolations { + filePath: string; + messages: Array<{ + ruleId: string; + message: string; + line: number; + column: number; + messageId: string; + endLine: number; + endColumn: number; + }>; +} diff --git a/test-helpers/expect-violations/tsconfig.json b/test-helpers/expect-violations/tsconfig.json new file mode 100644 index 0000000..6d9eab3 --- /dev/null +++ b/test-helpers/expect-violations/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "nodenext", + "target": "ES2015", + "outDir": "bin" + }, + "include": [ + "src/**/*.ts", + ], + } + \ No newline at end of file diff --git a/tests/basic_javascript_eslint8/.eslintrc.js b/tests/basic_javascript_eslint8/.eslintrc.js new file mode 100644 index 0000000..89c6e81 --- /dev/null +++ b/tests/basic_javascript_eslint8/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + "root": true, // Tell eslint to not look for config file up the hierarchy + "extends": [ + // "eslint:recommended", + // "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "EXPERIMENTAL_useProjectService": true + }, + "plugins": [ + // "@typescript-eslint", + "eslint-plugin-safety-web" + ], + "rules": { + "safety-web/trusted-types-checks": "error" + } +}; diff --git a/tests/basic_javascript_eslint8/README.md b/tests/basic_javascript_eslint8/README.md new file mode 100644 index 0000000..cb039bb --- /dev/null +++ b/tests/basic_javascript_eslint8/README.md @@ -0,0 +1,3 @@ +Test project used as an integration test for safety-web. + +This JavaScript project is set up with ESLint 9. It uses the `EXPERIMENTAL_useProjectService` which uses the TS language server to process JavaScript code with an inferred tsconfig (similar to what VSCode does). See https://typescript-eslint.io/packages/parser/#experimental_useprojectservice. diff --git a/tests/basic_javascript_eslint8/expected_violations.json b/tests/basic_javascript_eslint8/expected_violations.json new file mode 100644 index 0000000..4c9ecd0 --- /dev/null +++ b/tests/basic_javascript_eslint8/expected_violations.json @@ -0,0 +1,16 @@ +[ + { + "filePath": "[...]/index.js", + "messages": [ + { + "ruleId": "safety-web/trusted-types-checks", + "message": "[ban-element-innerhtml-assignments] Assigning directly to Element#innerHTML can result in XSS vulnerabilities.", + "line": 15, + "column": 1, + "messageId": "ban_element_innerhtml_assignments", + "endLine": 15, + "endColumn": 48 + } + ] + } +] \ No newline at end of file diff --git a/tests/basic_javascript_eslint8/index.js b/tests/basic_javascript_eslint8/index.js new file mode 100644 index 0000000..e111422 --- /dev/null +++ b/tests/basic_javascript_eslint8/index.js @@ -0,0 +1,15 @@ +// Copyright 2024 Google LLC +// +// 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 +// +// https://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. + +document.createElement('div').innerHTML = 'foo'; diff --git a/tests/basic_javascript_eslint8/package.json b/tests/basic_javascript_eslint8/package.json new file mode 100644 index 0000000..d6c763d --- /dev/null +++ b/tests/basic_javascript_eslint8/package.json @@ -0,0 +1,18 @@ +{ + "name": "basic-javascript-eslint8", + "version": "0.0.1", + "private": true, + "scripts": { + "clean": ":", + "build": ":", + "lint": "echo skip lint script for integration test case", + "test": "eslint --version | grep 'v8' && eslint index.js --format json | yarn run expect-violations test ./expected_violations.json", + "update": "eslint index.js --format json | yarn run expect-violations update ./expected_violations.json" + }, + "devDependencies": { + "eslint": "^8.0.0", + "eslint-plugin-safety-web": "workspace:^", + "expect-violations": "workspace:^", + "typescript-eslint": "^7.17.0" + } +} diff --git a/tests/basic_javascript_eslint9/README.md b/tests/basic_javascript_eslint9/README.md new file mode 100644 index 0000000..cb039bb --- /dev/null +++ b/tests/basic_javascript_eslint9/README.md @@ -0,0 +1,3 @@ +Test project used as an integration test for safety-web. + +This JavaScript project is set up with ESLint 9. It uses the `EXPERIMENTAL_useProjectService` which uses the TS language server to process JavaScript code with an inferred tsconfig (similar to what VSCode does). See https://typescript-eslint.io/packages/parser/#experimental_useprojectservice. diff --git a/tests/basic_javascript_eslint9/eslint.config.js b/tests/basic_javascript_eslint9/eslint.config.js new file mode 100644 index 0000000..c8c1611 --- /dev/null +++ b/tests/basic_javascript_eslint9/eslint.config.js @@ -0,0 +1,20 @@ +import tseslint from 'typescript-eslint'; +import safetyWeb from 'eslint-plugin-safety-web'; + +export default [ + { + plugins: { + "safety-web": safetyWeb + }, + rules: { + "safety-web/trusted-types-checks": "error" + }, + files: ["**/*.ts", "**/*.js"], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + EXPERIMENTAL_useProjectService: true, + } + } + } +]; diff --git a/tests/basic_javascript_eslint9/expected_violations.json b/tests/basic_javascript_eslint9/expected_violations.json new file mode 100644 index 0000000..a8b22cc --- /dev/null +++ b/tests/basic_javascript_eslint9/expected_violations.json @@ -0,0 +1,16 @@ +[ + { + "filePath": "[...]/index.js", + "messages": [ + { + "ruleId": "safety-web/trusted-types-checks", + "message": "[ban-element-innerhtml-assignments] Assigning directly to Element#innerHTML can result in XSS vulnerabilities.", + "line": 15, + "column": 1, + "messageId": "ban_element_innerhtml_assignments", + "endLine": 15, + "endColumn": 51 + } + ] + } +] \ No newline at end of file diff --git a/tests/basic_javascript_eslint9/index.js b/tests/basic_javascript_eslint9/index.js new file mode 100644 index 0000000..f5f9ea4 --- /dev/null +++ b/tests/basic_javascript_eslint9/index.js @@ -0,0 +1,15 @@ +// Copyright 2024 Google LLC +// +// 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 +// +// https://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. + +document.createElement('script').innerHTML = 'foo'; diff --git a/tests/basic_javascript_eslint9/package.json b/tests/basic_javascript_eslint9/package.json new file mode 100644 index 0000000..2b34446 --- /dev/null +++ b/tests/basic_javascript_eslint9/package.json @@ -0,0 +1,19 @@ +{ + "name": "basic-javascript-eslint9", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "clean": ":", + "build": ":", + "lint": "echo skip lint script for integration test case", + "test": "eslint --format json | yarn run expect-violations test ./expected_violations.json", + "update": "eslint --format json | yarn run expect-violations update ./expected_violations.json" + }, + "devDependencies": { + "eslint": "^9.6.0", + "eslint-plugin-safety-web": "workspace:^", + "expect-violations": "workspace:^", + "typescript-eslint": "^7.17.0" + } +} diff --git a/tests/basic_typescript_eslint8/.eslintrc.json b/tests/basic_typescript_eslint8/.eslintrc.json new file mode 100644 index 0000000..d297726 --- /dev/null +++ b/tests/basic_typescript_eslint8/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "root": true, // Tell eslint to not look for config file up the hierarchy + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": [ + "./tsconfig.json" + ] + }, + "plugins": [ + "@typescript-eslint", + "eslint-plugin-safety-web" + ], + "rules": { + "safety-web/trusted-types-checks": "error" + } +} \ No newline at end of file diff --git a/tests/basic_typescript_eslint8/expected_violations.json b/tests/basic_typescript_eslint8/expected_violations.json new file mode 100644 index 0000000..82bab61 --- /dev/null +++ b/tests/basic_typescript_eslint8/expected_violations.json @@ -0,0 +1,16 @@ +[ + { + "filePath": "[...]/index.ts", + "messages": [ + { + "ruleId": "safety-web/trusted-types-checks", + "message": "[ban-element-innerhtml-assignments] Assigning directly to Element#innerHTML can result in XSS vulnerabilities.", + "line": 15, + "column": 1, + "messageId": "ban_element_innerhtml_assignments", + "endLine": 15, + "endColumn": 51 + } + ] + } +] \ No newline at end of file diff --git a/tests/basic_typescript_eslint8/package.json b/tests/basic_typescript_eslint8/package.json new file mode 100644 index 0000000..69a360b --- /dev/null +++ b/tests/basic_typescript_eslint8/package.json @@ -0,0 +1,20 @@ +{ + "name": "basic-typescript-eslint8", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "clean": "tsc --build --clean", + "build": "tsc", + "lint": "echo skip lint script for integration test case", + "test": "eslint --version | grep 'v8' && eslint src/ --format json | yarn run expect-violations test ./expected_violations.json", + "update": "eslint src/ --format json | yarn run expect-violations update ./expected_violations.json" + }, + "devDependencies": { + "eslint": "^8.0.0", + "eslint-plugin-safety-web": "workspace:^", + "expect-violations": "workspace:^", + "typescript": "^5.4.3 <5.5.0", + "typescript-eslint": "^7.17.0" + } +} diff --git a/tests/basic_typescript_eslint8/src/index.ts b/tests/basic_typescript_eslint8/src/index.ts new file mode 100644 index 0000000..f5f9ea4 --- /dev/null +++ b/tests/basic_typescript_eslint8/src/index.ts @@ -0,0 +1,15 @@ +// Copyright 2024 Google LLC +// +// 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 +// +// https://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. + +document.createElement('script').innerHTML = 'foo'; diff --git a/tests/basic_typescript_eslint8/tsconfig.json b/tests/basic_typescript_eslint8/tsconfig.json new file mode 100644 index 0000000..eaa63b6 --- /dev/null +++ b/tests/basic_typescript_eslint8/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "nodenext", + "target": "ES2015", + "outDir": "lib", + "lib": [ + "DOM" + ], + }, + "include": [ + "src/**/*.ts", + ], +} \ No newline at end of file diff --git a/tests/basic_typescript_eslint9/eslint.config.js b/tests/basic_typescript_eslint9/eslint.config.js new file mode 100644 index 0000000..7aee10c --- /dev/null +++ b/tests/basic_typescript_eslint9/eslint.config.js @@ -0,0 +1,29 @@ +import tseslint from 'typescript-eslint'; +import safetyWeb from 'eslint-plugin-safety-web'; + +// https://typescript-eslint.io/getting-started/typed-linting/ +export default tseslint.config( + { + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: true, // Indicates to find the closest tsconfig.json for each source file (see https://typescript-eslint.io/packages/parser#project). + tsconfigRootDir: import.meta.dirname, + }, + }, + files: ["src/**/*.ts"], + plugins: { + "safety-web": safetyWeb + }, + rules: { + "safety-web/trusted-types-checks": "error" + } + }, + // Disable undef in TS https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors + { + files: ['src/**/*.{ts,tsx,mts,cts}'], + rules: { + 'no-undef': 'off', + }, + }, +); diff --git a/tests/basic_typescript_eslint9/expected_violations.json b/tests/basic_typescript_eslint9/expected_violations.json new file mode 100644 index 0000000..82bab61 --- /dev/null +++ b/tests/basic_typescript_eslint9/expected_violations.json @@ -0,0 +1,16 @@ +[ + { + "filePath": "[...]/index.ts", + "messages": [ + { + "ruleId": "safety-web/trusted-types-checks", + "message": "[ban-element-innerhtml-assignments] Assigning directly to Element#innerHTML can result in XSS vulnerabilities.", + "line": 15, + "column": 1, + "messageId": "ban_element_innerhtml_assignments", + "endLine": 15, + "endColumn": 51 + } + ] + } +] \ No newline at end of file diff --git a/tests/basic_typescript_eslint9/package.json b/tests/basic_typescript_eslint9/package.json new file mode 100644 index 0000000..adb09ca --- /dev/null +++ b/tests/basic_typescript_eslint9/package.json @@ -0,0 +1,20 @@ +{ + "name": "basic-typescript-eslint9", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "clean": "tsc --build --clean", + "build": "tsc", + "lint": "echo skip lint script for integration test case", + "test": "eslint --format json | yarn run expect-violations test ./expected_violations.json", + "update": "eslint --format json | yarn run expect-violations update ./expected_violations.json" + }, + "devDependencies": { + "eslint": "^9.6.0", + "eslint-plugin-safety-web": "workspace:^", + "expect-violations": "workspace:^", + "typescript": "^5.4.3 <5.5.0", + "typescript-eslint": "^7.17.0" + } +} diff --git a/tests/basic_typescript_eslint9/src/index.ts b/tests/basic_typescript_eslint9/src/index.ts new file mode 100644 index 0000000..f5f9ea4 --- /dev/null +++ b/tests/basic_typescript_eslint9/src/index.ts @@ -0,0 +1,15 @@ +// Copyright 2024 Google LLC +// +// 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 +// +// https://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. + +document.createElement('script').innerHTML = 'foo'; diff --git a/tests/basic_typescript_eslint9/tsconfig.json b/tests/basic_typescript_eslint9/tsconfig.json new file mode 100644 index 0000000..eaa63b6 --- /dev/null +++ b/tests/basic_typescript_eslint9/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "nodenext", + "target": "ES2015", + "outDir": "lib", + "lib": [ + "DOM" + ], + }, + "include": [ + "src/**/*.ts", + ], +} \ No newline at end of file diff --git a/update_tsetse.sh b/update_tsetse.sh new file mode 100644 index 0000000..aa079de --- /dev/null +++ b/update_tsetse.sh @@ -0,0 +1,31 @@ +# Copyright 2024 Google LLC +# +# 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 +# +# https://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. + +#!/bin/bash + +SAFETY_WEB_GIT_ROOT="$(dirname $0)" +LOG_FILE="${SAFETY_WEB_GIT_ROOT}/safety-web/update_tsetse_logs.txt" + +## Update the vendored copy of tsetse, from the latest tsec commit. +TMPDIR="$(mktemp -d)" +trap 'rm -rf -- "$TMPDIR"' EXIT +git clone https://github.com/google/tsec.git "${TMPDIR}" +rm -rf "${SAFETY_WEB_GIT_ROOT}/safety-web/src/common" +cp -r "${TMPDIR}/common/" "${SAFETY_WEB_GIT_ROOT}/safety-web/src/" + +echo \ +"Most recent run of update_tsetse.sh: $(date) +--- +Most recent tsetse commit (from https://github.com/google/tsec.git):" > $LOG_FILE +git -C "${TMPDIR}/" log -n 1 >> $LOG_FILE diff --git a/yarn.lock b/yarn.lock index fb57ccd..091c1ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,4 +1,3055 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! +__metadata: + version: 8 + cacheKey: 10c0 +"@cspotcode/source-map-support@npm:^0.8.0": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1" + dependencies: + "@jridgewell/trace-mapping": "npm:0.3.9" + checksum: 10c0/05c5368c13b662ee4c122c7bfbe5dc0b613416672a829f3e78bc49a357a197e0218d6e74e7c66cfcd04e15a179acab080bd3c69658c9fbefd0e1ccd950a07fc6 + languageName: node + linkType: hard + +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": + version: 4.4.0 + resolution: "@eslint-community/eslint-utils@npm:4.4.0" + dependencies: + eslint-visitor-keys: "npm:^3.3.0" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10c0/7e559c4ce59cd3a06b1b5a517b593912e680a7f981ae7affab0d01d709e99cd5647019be8fafa38c350305bc32f1f7d42c7073edde2ab536c745e365f37b607e + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.6.1": + version: 4.11.0 + resolution: "@eslint-community/regexpp@npm:4.11.0" + checksum: 10c0/0f6328869b2741e2794da4ad80beac55cba7de2d3b44f796a60955b0586212ec75e6b0253291fd4aad2100ad471d1480d8895f2b54f1605439ba4c875e05e523 + languageName: node + linkType: hard + +"@eslint/config-array@npm:^0.17.0": + version: 0.17.0 + resolution: "@eslint/config-array@npm:0.17.0" + dependencies: + "@eslint/object-schema": "npm:^2.1.4" + debug: "npm:^4.3.1" + minimatch: "npm:^3.1.2" + checksum: 10c0/97eb23ef0948dbc5f24884a3b75c537ca37ee2b1f27a864cd0d9189c089bc1a724dc6e1a4d9b7dd304d9f732ca02aa7916243a7715d6f1f17159d8a8c83f0c9e + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/eslintrc@npm:2.1.4" + dependencies: + ajv: "npm:^6.12.4" + debug: "npm:^4.3.2" + espree: "npm:^9.6.0" + globals: "npm:^13.19.0" + ignore: "npm:^5.2.0" + import-fresh: "npm:^3.2.1" + js-yaml: "npm:^4.1.0" + minimatch: "npm:^3.1.2" + strip-json-comments: "npm:^3.1.1" + checksum: 10c0/32f67052b81768ae876c84569ffd562491ec5a5091b0c1e1ca1e0f3c24fb42f804952fdd0a137873bc64303ba368a71ba079a6f691cee25beee9722d94cc8573 + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^3.1.0": + version: 3.1.0 + resolution: "@eslint/eslintrc@npm:3.1.0" + dependencies: + ajv: "npm:^6.12.4" + debug: "npm:^4.3.2" + espree: "npm:^10.0.1" + globals: "npm:^14.0.0" + ignore: "npm:^5.2.0" + import-fresh: "npm:^3.2.1" + js-yaml: "npm:^4.1.0" + minimatch: "npm:^3.1.2" + strip-json-comments: "npm:^3.1.1" + checksum: 10c0/5b7332ed781edcfc98caa8dedbbb843abfb9bda2e86538529c843473f580e40c69eb894410eddc6702f487e9ee8f8cfa8df83213d43a8fdb549f23ce06699167 + languageName: node + linkType: hard + +"@eslint/js@npm:8.57.0": + version: 8.57.0 + resolution: "@eslint/js@npm:8.57.0" + checksum: 10c0/9a518bb8625ba3350613903a6d8c622352ab0c6557a59fe6ff6178bf882bf57123f9d92aa826ee8ac3ee74b9c6203fe630e9ee00efb03d753962dcf65ee4bd94 + languageName: node + linkType: hard + +"@eslint/js@npm:9.6.0": + version: 9.6.0 + resolution: "@eslint/js@npm:9.6.0" + checksum: 10c0/83967a7e59f2e958c9bbb3acd0929cad00d59d927ad786ed8e0d30b07f983c6bea3af6f4ad32da32145db40b7a741a816ba339bdd8960fc7fc8231716d943b7f + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/object-schema@npm:2.1.4" + checksum: 10c0/e9885532ea70e483fb007bf1275968b05bb15ebaa506d98560c41a41220d33d342e19023d5f2939fed6eb59676c1bda5c847c284b4b55fce521d282004da4dda + languageName: node + linkType: hard + +"@humanwhocodes/config-array@npm:^0.11.14": + version: 0.11.14 + resolution: "@humanwhocodes/config-array@npm:0.11.14" + dependencies: + "@humanwhocodes/object-schema": "npm:^2.0.2" + debug: "npm:^4.3.1" + minimatch: "npm:^3.0.5" + checksum: 10c0/66f725b4ee5fdd8322c737cb5013e19fac72d4d69c8bf4b7feb192fcb83442b035b92186f8e9497c220e58b2d51a080f28a73f7899bc1ab288c3be172c467541 + languageName: node + linkType: hard + +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 10c0/909b69c3b86d482c26b3359db16e46a32e0fb30bd306a3c176b8313b9e7313dba0f37f519de6aa8b0a1921349e505f259d19475e123182416a506d7f87e7f529 + languageName: node + linkType: hard + +"@humanwhocodes/object-schema@npm:^2.0.2": + version: 2.0.3 + resolution: "@humanwhocodes/object-schema@npm:2.0.3" + checksum: 10c0/80520eabbfc2d32fe195a93557cef50dfe8c8905de447f022675aaf66abc33ae54098f5ea78548d925aa671cd4ab7c7daa5ad704fe42358c9b5e7db60f80696c + languageName: node + linkType: hard + +"@humanwhocodes/retry@npm:^0.3.0": + version: 0.3.0 + resolution: "@humanwhocodes/retry@npm:0.3.0" + checksum: 10c0/7111ec4e098b1a428459b4e3be5a5d2a13b02905f805a2468f4fa628d072f0de2da26a27d04f65ea2846f73ba51f4204661709f05bfccff645e3cedef8781bb6 + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 10c0/b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.0.3": + version: 3.1.2 + resolution: "@jridgewell/resolve-uri@npm:3.1.2" + checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10": + version: 1.5.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" + checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: 10c0/fa425b606d7c7ee5bfa6a31a7b050dd5814b4082f318e0e4190f991902181b4330f43f4805db1dd4f2433fd0ed9cc7a7b9c2683f1deeab1df1b0a98b1e24055b + languageName: node + linkType: hard + +"@nodelib/fs.scandir@npm:2.1.5": + version: 2.1.5 + resolution: "@nodelib/fs.scandir@npm:2.1.5" + dependencies: + "@nodelib/fs.stat": "npm:2.0.5" + run-parallel: "npm:^1.1.9" + checksum: 10c0/732c3b6d1b1e967440e65f284bd06e5821fedf10a1bea9ed2bb75956ea1f30e08c44d3def9d6a230666574edbaf136f8cfd319c14fd1f87c66e6a44449afb2eb + languageName: node + linkType: hard + +"@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": + version: 2.0.5 + resolution: "@nodelib/fs.stat@npm:2.0.5" + checksum: 10c0/88dafe5e3e29a388b07264680dc996c17f4bda48d163a9d4f5c1112979f0ce8ec72aa7116122c350b4e7976bc5566dc3ddb579be1ceaacc727872eb4ed93926d + languageName: node + linkType: hard + +"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": + version: 1.2.8 + resolution: "@nodelib/fs.walk@npm:1.2.8" + dependencies: + "@nodelib/fs.scandir": "npm:2.1.5" + fastq: "npm:^1.6.0" + checksum: 10c0/db9de047c3bb9b51f9335a7bb46f4fcfb6829fb628318c12115fbaf7d369bfce71c15b103d1fc3b464812d936220ee9bc1c8f762d032c9f6be9acc99249095b1 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^2.0.0": + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/c37a5b4842bfdece3d14dfdb054f73fe15ed2d3da61b34ff76629fb5b1731647c49166fd2a8bf8b56fcfa51200382385ea8909a3cbecdad612310c114d3f6c99 + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node10@npm:1.0.11" + checksum: 10c0/28a0710e5d039e0de484bdf85fee883bfd3f6a8980601f4d44066b0a6bcd821d31c4e231d1117731c4e24268bd4cf2a788a6787c12fc7f8d11014c07d582783c + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11" + checksum: 10c0/dddca2b553e2bee1308a056705103fc8304e42bb2d2cbd797b84403a223b25c78f2c683ec3e24a095e82cd435387c877239bffcb15a590ba817cd3f6b9a99fd9 + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3" + checksum: 10c0/67c1316d065fdaa32525bc9449ff82c197c4c19092b9663b23213c8cbbf8d88b6ed6a17898e0cbc2711950fbfaf40388938c1c748a2ee89f7234fc9e7fe2bf44 + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4" + checksum: 10c0/05f8f2734e266fb1839eb1d57290df1664fe2aa3b0fdd685a9035806daa635f7519bf6d5d9b33f6e69dd545b8c46bd6e2b5c79acb2b1f146e885f7f11a42a5bb + languageName: node + linkType: hard + +"@types/chai@npm:^4.3.16": + version: 4.3.16 + resolution: "@types/chai@npm:4.3.16" + checksum: 10c0/745d4a9be429d5d86a7ab26064610b8957fe12dd80e94dc7d0707cf3db1c889e3ffe0d73d69bb15e6d376bf4462a7a75e9d8fc1051750b5d656d6cfe459829b7 + languageName: node + linkType: hard + +"@types/mocha@npm:^10.0.7": + version: 10.0.7 + resolution: "@types/mocha@npm:10.0.7" + checksum: 10c0/48a2df4dd02b6e66a11129dca6a23cf0cc3995faf8525286eb851043685bd8b7444780f4bb29a1c42df7559ed63294e5308bfce3a6b862ad2e0359cb21c21329 + languageName: node + linkType: hard + +"@types/node@npm:^20.14.9": + version: 20.14.9 + resolution: "@types/node@npm:20.14.9" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/911ffa444dc032897f4a23ed580c67903bd38ea1c5ec99b1d00fa10b83537a3adddef8e1f29710cbdd8e556a61407ed008e06537d834e48caf449ce59f87d387 + languageName: node + linkType: hard + +"@types/yargs-parser@npm:*": + version: 21.0.3 + resolution: "@types/yargs-parser@npm:21.0.3" + checksum: 10c0/e71c3bd9d0b73ca82e10bee2064c384ab70f61034bbfb78e74f5206283fc16a6d85267b606b5c22cb2a3338373586786fed595b2009825d6a9115afba36560a0 + languageName: node + linkType: hard + +"@types/yargs@npm:^17.0.32": + version: 17.0.32 + resolution: "@types/yargs@npm:17.0.32" + dependencies: + "@types/yargs-parser": "npm:*" + checksum: 10c0/2095e8aad8a4e66b86147415364266b8d607a3b95b4239623423efd7e29df93ba81bb862784a6e08664f645cc1981b25fd598f532019174cd3e5e1e689e1cccf + languageName: node + linkType: hard + +"@typescript-eslint/eslint-plugin@npm:7.17.0": + version: 7.17.0 + resolution: "@typescript-eslint/eslint-plugin@npm:7.17.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.10.0" + "@typescript-eslint/scope-manager": "npm:7.17.0" + "@typescript-eslint/type-utils": "npm:7.17.0" + "@typescript-eslint/utils": "npm:7.17.0" + "@typescript-eslint/visitor-keys": "npm:7.17.0" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.3.1" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^1.3.0" + peerDependencies: + "@typescript-eslint/parser": ^7.0.0 + eslint: ^8.56.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/654d589531ae45b8ca8f3969e785926b2544100a985968d86c828e2a1ff50331250e19c8b4af83a4ba17847a0047479662eb317e4ad94f6279cac03acd5cda5a + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:7.17.0, @typescript-eslint/parser@npm:^7.17.0": + version: 7.17.0 + resolution: "@typescript-eslint/parser@npm:7.17.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:7.17.0" + "@typescript-eslint/types": "npm:7.17.0" + "@typescript-eslint/typescript-estree": "npm:7.17.0" + "@typescript-eslint/visitor-keys": "npm:7.17.0" + debug: "npm:^4.3.4" + peerDependencies: + eslint: ^8.56.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/0cf6922412517b4c005609b035119ddd2798e1b6e74e1bccd487aa53119d27067cfd89311f00b8e96b2b044a0fb7373418a16552be86079879158b260c397418 + languageName: node + linkType: hard + +"@typescript-eslint/rule-tester@npm:^7.17.0": + version: 7.17.0 + resolution: "@typescript-eslint/rule-tester@npm:7.17.0" + dependencies: + "@typescript-eslint/typescript-estree": "npm:7.17.0" + "@typescript-eslint/utils": "npm:7.17.0" + ajv: "npm:^6.12.6" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + lodash.merge: "npm:4.6.2" + semver: "npm:^7.6.0" + peerDependencies: + "@eslint/eslintrc": ">=2" + eslint: ^8.56.0 + checksum: 10c0/c6d4458ff911dc374ccb2c78ed7ecaa10201256f1201602bbd7a1c61533646508cb40b2b9a8ff97936c2e04b9348bd01321a595d7f7bd17508dce8831cd332d3 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:7.17.0": + version: 7.17.0 + resolution: "@typescript-eslint/scope-manager@npm:7.17.0" + dependencies: + "@typescript-eslint/types": "npm:7.17.0" + "@typescript-eslint/visitor-keys": "npm:7.17.0" + checksum: 10c0/e1a693e19dc855fe6d04b46c6c205019bfc937eda5f8b255393f8267ebddd282165568336e37b04aab544b155a807784b9c4a92129dfc7c1eef5a9e9fe052685 + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:7.17.0": + version: 7.17.0 + resolution: "@typescript-eslint/type-utils@npm:7.17.0" + dependencies: + "@typescript-eslint/typescript-estree": "npm:7.17.0" + "@typescript-eslint/utils": "npm:7.17.0" + debug: "npm:^4.3.4" + ts-api-utils: "npm:^1.3.0" + peerDependencies: + eslint: ^8.56.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/b415cf37c0922cded78735c5049cb5a5b0065e1c0ce4a81ca2a26422763ccacca8945efa45480f40530f2ec414a14d35a88a6798258aa889f7a9cf4ca4a240cd + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:7.17.0": + version: 7.17.0 + resolution: "@typescript-eslint/types@npm:7.17.0" + checksum: 10c0/8f734294d432b37c534f17eb2befdfe43b76874d09118d6adf7e308e5a586e9e11b7021abe4f6692a6e6226de58a15b3cfe1300939556ce1c908d9af627b7400 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:7.17.0": + version: 7.17.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.17.0" + dependencies: + "@typescript-eslint/types": "npm:7.17.0" + "@typescript-eslint/visitor-keys": "npm:7.17.0" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/10967823ce00c9f8cd4a8b56bed3524c098e38cc0e27aaa49ffd8fad4e671c00226bf0330ba858948750b88dc55527ebeb62c74be8a30bac18a106d6c033ab59 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:7.17.0, @typescript-eslint/utils@npm:^7.17.0": + version: 7.17.0 + resolution: "@typescript-eslint/utils@npm:7.17.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:7.17.0" + "@typescript-eslint/types": "npm:7.17.0" + "@typescript-eslint/typescript-estree": "npm:7.17.0" + peerDependencies: + eslint: ^8.56.0 + checksum: 10c0/1f3e22820b3ab3e47809c45e576614ad4a965f5c8634856eca5c70981386b9351a77fb172ba32345e7c5667479cf9526c673699dd38dccd0616ad6db21704e72 + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:7.17.0": + version: 7.17.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.17.0" + dependencies: + "@typescript-eslint/types": "npm:7.17.0" + eslint-visitor-keys: "npm:^3.4.3" + checksum: 10c0/fa6b339d51fc3710288bb2ffaa46d639551d77965cc42c36f96c4f43aed663ff12972e8a28652a280f6ce20b7a92dc2aea14b2b4049012799be2fc2d3cbb2c60 + languageName: node + linkType: hard + +"@ungap/structured-clone@npm:^1.2.0": + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 10c0/8209c937cb39119f44eb63cf90c0b73e7c754209a6411c707be08e50e29ee81356dca1a848a405c8bdeebfe2f5e4f831ad310ae1689eeef65e7445c090c6657d + languageName: node + linkType: hard + +"abbrev@npm:^2.0.0": + version: 2.0.0 + resolution: "abbrev@npm:2.0.0" + checksum: 10c0/f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 + languageName: node + linkType: hard + +"acorn-jsx@npm:^5.3.2": + version: 5.3.2 + resolution: "acorn-jsx@npm:5.3.2" + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/4c54868fbef3b8d58927d5e33f0a4de35f59012fe7b12cf9dfbb345fb8f46607709e1c4431be869a23fb63c151033d84c4198fa9f79385cec34fcb1dd53974c1 + languageName: node + linkType: hard + +"acorn-walk@npm:^8.1.1": + version: 8.3.3 + resolution: "acorn-walk@npm:8.3.3" + dependencies: + acorn: "npm:^8.11.0" + checksum: 10c0/4a9e24313e6a0a7b389e712ba69b66b455b4cb25988903506a8d247e7b126f02060b05a8a5b738a9284214e4ca95f383dd93443a4ba84f1af9b528305c7f243b + languageName: node + linkType: hard + +"acorn@npm:^8.11.0, acorn@npm:^8.12.0, acorn@npm:^8.4.1": + version: 8.12.1 + resolution: "acorn@npm:8.12.1" + bin: + acorn: bin/acorn + checksum: 10c0/51fb26cd678f914e13287e886da2d7021f8c2bc0ccc95e03d3e0447ee278dd3b40b9c57dc222acd5881adcf26f3edc40901a4953403232129e3876793cd17386 + languageName: node + linkType: hard + +"acorn@npm:^8.9.0": + version: 8.12.0 + resolution: "acorn@npm:8.12.0" + bin: + acorn: bin/acorn + checksum: 10c0/a19f9dead009d3b430fa3c253710b47778cdaace15b316de6de93a68c355507bc1072a9956372b6c990cbeeb167d4a929249d0faeb8ae4bb6911d68d53299549 + languageName: node + linkType: hard + +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" + dependencies: + debug: "npm:^4.3.4" + checksum: 10c0/e59ce7bed9c63bf071a30cc471f2933862044c97fd9958967bfe22521d7a0f601ce4ed5a8c011799d0c726ca70312142ae193bbebb60f576b52be19d4a363b50 + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: 10c0/a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 + languageName: node + linkType: hard + +"ajv@npm:^6.12.4, ajv@npm:^6.12.6": + version: 6.12.6 + resolution: "ajv@npm:6.12.6" + dependencies: + fast-deep-equal: "npm:^3.1.1" + fast-json-stable-stringify: "npm:^2.0.0" + json-schema-traverse: "npm:^0.4.1" + uri-js: "npm:^4.2.2" + checksum: 10c0/41e23642cbe545889245b9d2a45854ebba51cda6c778ebced9649420d9205f2efb39cb43dbc41e358409223b1ea43303ae4839db682c848b891e4811da1a5a71 + languageName: node + linkType: hard + +"ansi-colors@npm:^4.1.3": + version: 4.1.3 + resolution: "ansi-colors@npm:4.1.3" + checksum: 10c0/ec87a2f59902f74e61eada7f6e6fe20094a628dab765cfdbd03c3477599368768cffccdb5d3bb19a1b6c99126783a143b1fee31aab729b31ffe5836c7e5e28b9 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 10c0/cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + languageName: node + linkType: hard + +"anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + languageName: node + linkType: hard + +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 10c0/070ff801a9d236a6caa647507bdcc7034530604844d64408149a26b9e87c2f97650055c0f049abd1efc024b334635c01f29e0b632b371ac3f26130f4cf65997a + languageName: node + linkType: hard + +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e + languageName: node + linkType: hard + +"array-union@npm:^2.1.0": + version: 2.1.0 + resolution: "array-union@npm:2.1.0" + checksum: 10c0/429897e68110374f39b771ec47a7161fc6a8fc33e196857c0a396dc75df0b5f65e4d046674db764330b6bb66b39ef48dd7c53b6a2ee75cfb0681e0c1a7033962 + languageName: node + linkType: hard + +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + languageName: node + linkType: hard + +"basic-javascript-eslint8@workspace:tests/basic_javascript_eslint8": + version: 0.0.0-use.local + resolution: "basic-javascript-eslint8@workspace:tests/basic_javascript_eslint8" + dependencies: + eslint: "npm:^8.0.0" + eslint-plugin-safety-web: "workspace:^" + expect-violations: "workspace:^" + typescript-eslint: "npm:^7.17.0" + languageName: unknown + linkType: soft + +"basic-javascript-eslint9@workspace:tests/basic_javascript_eslint9": + version: 0.0.0-use.local + resolution: "basic-javascript-eslint9@workspace:tests/basic_javascript_eslint9" + dependencies: + eslint: "npm:^9.6.0" + eslint-plugin-safety-web: "workspace:^" + expect-violations: "workspace:^" + typescript-eslint: "npm:^7.17.0" + languageName: unknown + linkType: soft + +"basic-typescript-eslint8@workspace:tests/basic_typescript_eslint8": + version: 0.0.0-use.local + resolution: "basic-typescript-eslint8@workspace:tests/basic_typescript_eslint8" + dependencies: + eslint: "npm:^8.0.0" + eslint-plugin-safety-web: "workspace:^" + expect-violations: "workspace:^" + typescript: "npm:^5.4.3 <5.5.0" + typescript-eslint: "npm:^7.17.0" + languageName: unknown + linkType: soft + +"basic-typescript-eslint9@workspace:tests/basic_typescript_eslint9": + version: 0.0.0-use.local + resolution: "basic-typescript-eslint9@workspace:tests/basic_typescript_eslint9" + dependencies: + eslint: "npm:^9.6.0" + eslint-plugin-safety-web: "workspace:^" + expect-violations: "workspace:^" + typescript: "npm:^5.4.3 <5.5.0" + typescript-eslint: "npm:^7.17.0" + languageName: unknown + linkType: soft + +"binary-extensions@npm:^2.0.0": + version: 2.3.0 + resolution: "binary-extensions@npm:2.3.0" + checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.11 + resolution: "brace-expansion@npm:1.1.11" + dependencies: + balanced-match: "npm:^1.0.0" + concat-map: "npm:0.0.1" + checksum: 10c0/695a56cd058096a7cb71fb09d9d6a7070113c7be516699ed361317aca2ec169f618e28b8af352e02ab4233fb54eb0168460a40dc320bab0034b36ab59aaad668 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + languageName: node + linkType: hard + +"braces@npm:^3.0.3, braces@npm:~3.0.2": + version: 3.0.3 + resolution: "braces@npm:3.0.3" + dependencies: + fill-range: "npm:^7.1.1" + checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04 + languageName: node + linkType: hard + +"browser-stdout@npm:^1.3.1": + version: 1.3.1 + resolution: "browser-stdout@npm:1.3.1" + checksum: 10c0/c40e482fd82be872b6ea7b9f7591beafbf6f5ba522fe3dade98ba1573a1c29a11101564993e4eb44e5488be8f44510af072df9a9637c739217eb155ceb639205 + languageName: node + linkType: hard + +"cacache@npm:^18.0.0": + version: 18.0.4 + resolution: "cacache@npm:18.0.4" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: 10c0/6c055bafed9de4f3dcc64ac3dc7dd24e863210902b7c470eb9ce55a806309b3efff78033e3d8b4f7dcc5d467f2db43c6a2857aaaf26f0094b8a351d44c42179f + languageName: node + linkType: hard + +"callsites@npm:^3.0.0": + version: 3.1.0 + resolution: "callsites@npm:3.1.0" + checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301 + languageName: node + linkType: hard + +"camelcase@npm:^6.0.0": + version: 6.3.0 + resolution: "camelcase@npm:6.3.0" + checksum: 10c0/0d701658219bd3116d12da3eab31acddb3f9440790c0792e0d398f0a520a6a4058018e546862b6fba89d7ae990efaeb97da71e1913e9ebf5a8b5621a3d55c710 + languageName: node + linkType: hard + +"chai@npm:^5.1.1": + version: 5.1.1 + resolution: "chai@npm:5.1.1" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: 10c0/e7f00e5881e3d5224f08fe63966ed6566bd9fdde175863c7c16dd5240416de9b34c4a0dd925f4fd64ad56256ca6507d32cf6131c49e1db65c62578eb31d4566c + languageName: node + linkType: hard + +"chalk@npm:^4.0.0, chalk@npm:^4.1.0": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880 + languageName: node + linkType: hard + +"check-error@npm:^2.1.1": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: 10c0/979f13eccab306cf1785fa10941a590b4e7ea9916ea2a4f8c87f0316fc3eab07eabefb6e587424ef0f88cbcd3805791f172ea739863ca3d7ce2afc54641c7f0e + languageName: node + linkType: hard + +"chokidar@npm:^3.5.3": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: 10c0/594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 10c0/1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 + languageName: node + linkType: hard + +"cliui@npm:^7.0.2": + version: 7.0.4 + resolution: "cliui@npm:7.0.4" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.0" + wrap-ansi: "npm:^7.0.0" + checksum: 10c0/6035f5daf7383470cef82b3d3db00bec70afb3423538c50394386ffbbab135e26c3689c41791f911fa71b62d13d3863c712fdd70f0fbdffd938a1e6fd09aac00 + languageName: node + linkType: hard + +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 10c0/4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f + languageName: node + linkType: hard + +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: 10c0/157cbc59b2430ae9a90034a5f3a1b398b6738bf510f713edc4d4e45e169bc514d3d99dd34d8d01ca7ae7830b5b8b537e46ae8f3c8f932371b0875c0151d7ec91 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5": + version: 4.3.5 + resolution: "debug@npm:4.3.5" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/082c375a2bdc4f4469c99f325ff458adad62a3fc2c482d59923c260cb08152f34e2659f72b3767db8bb2f21ca81a60a42d1019605a412132d7b9f59363a005cc + languageName: node + linkType: hard + +"decamelize@npm:^4.0.0": + version: 4.0.0 + resolution: "decamelize@npm:4.0.0" + checksum: 10c0/e06da03fc05333e8cd2778c1487da67ffbea5b84e03ca80449519b8fa61f888714bbc6f459ea963d5641b4aa98832130eb5cd193d90ae9f0a27eee14be8e278d + languageName: node + linkType: hard + +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: 10c0/7102cf3b7bb719c6b9c0db2e19bf0aa9318d141581befe8c7ce8ccd39af9eaa4346e5e05adef7f9bd7015da0f13a3a25dcfe306ef79dc8668aedbecb658dd247 + languageName: node + linkType: hard + +"deep-is@npm:^0.1.3": + version: 0.1.4 + resolution: "deep-is@npm:0.1.4" + checksum: 10c0/7f0ee496e0dff14a573dc6127f14c95061b448b87b995fc96c017ce0a1e66af1675e73f1d6064407975bc4ea6ab679497a29fff7b5b9c4e99cb10797c1ad0b4c + languageName: node + linkType: hard + +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: 10c0/81b91f9d39c4eaca068eb0c1eb0e4afbdc5bb2941d197f513dd596b820b956fef43485876226d65d497bebc15666aa2aa82c679e84f65d5f2bfbf14ee46e32c1 + languageName: node + linkType: hard + +"diff@npm:^5.2.0": + version: 5.2.0 + resolution: "diff@npm:5.2.0" + checksum: 10c0/aed0941f206fe261ecb258dc8d0ceea8abbde3ace5827518ff8d302f0fc9cc81ce116c4d8f379151171336caf0516b79e01abdc1ed1201b6440d895a66689eb4 + languageName: node + linkType: hard + +"dir-glob@npm:^3.0.1": + version: 3.0.1 + resolution: "dir-glob@npm:3.0.1" + dependencies: + path-type: "npm:^4.0.0" + checksum: 10c0/dcac00920a4d503e38bb64001acb19df4efc14536ada475725e12f52c16777afdee4db827f55f13a908ee7efc0cb282e2e3dbaeeb98c0993dd93d1802d3bf00c + languageName: node + linkType: hard + +"doctrine@npm:^3.0.0": + version: 3.0.0 + resolution: "doctrine@npm:3.0.0" + dependencies: + esutils: "npm:^2.0.2" + checksum: 10c0/c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520 + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 10c0/26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 10c0/af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"esbuild@npm:~0.21.5": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": "npm:0.21.5" + "@esbuild/android-arm": "npm:0.21.5" + "@esbuild/android-arm64": "npm:0.21.5" + "@esbuild/android-x64": "npm:0.21.5" + "@esbuild/darwin-arm64": "npm:0.21.5" + "@esbuild/darwin-x64": "npm:0.21.5" + "@esbuild/freebsd-arm64": "npm:0.21.5" + "@esbuild/freebsd-x64": "npm:0.21.5" + "@esbuild/linux-arm": "npm:0.21.5" + "@esbuild/linux-arm64": "npm:0.21.5" + "@esbuild/linux-ia32": "npm:0.21.5" + "@esbuild/linux-loong64": "npm:0.21.5" + "@esbuild/linux-mips64el": "npm:0.21.5" + "@esbuild/linux-ppc64": "npm:0.21.5" + "@esbuild/linux-riscv64": "npm:0.21.5" + "@esbuild/linux-s390x": "npm:0.21.5" + "@esbuild/linux-x64": "npm:0.21.5" + "@esbuild/netbsd-x64": "npm:0.21.5" + "@esbuild/openbsd-x64": "npm:0.21.5" + "@esbuild/sunos-x64": "npm:0.21.5" + "@esbuild/win32-arm64": "npm:0.21.5" + "@esbuild/win32-ia32": "npm:0.21.5" + "@esbuild/win32-x64": "npm:0.21.5" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/fa08508adf683c3f399e8a014a6382a6b65542213431e26206c0720e536b31c09b50798747c2a105a4bbba1d9767b8d3615a74c2f7bf1ddf6d836cd11eb672de + languageName: node + linkType: hard + +"escalade@npm:^3.1.1": + version: 3.1.2 + resolution: "escalade@npm:3.1.2" + checksum: 10c0/6b4adafecd0682f3aa1cd1106b8fff30e492c7015b178bc81b2d2f75106dabea6c6d6e8508fc491bd58e597c74abb0e8e2368f943ecb9393d4162e3c2f3cf287 + languageName: node + linkType: hard + +"escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 10c0/9497d4dd307d845bd7f75180d8188bb17ea8c151c1edbf6b6717c100e104d629dc2dfb687686181b0f4b7d732c7dfdc4d5e7a8ff72de1b0ca283a75bbb3a9cd9 + languageName: node + linkType: hard + +"eslint-plugin-safety-web@workspace:^, eslint-plugin-safety-web@workspace:safety-web": + version: 0.0.0-use.local + resolution: "eslint-plugin-safety-web@workspace:safety-web" + dependencies: + "@eslint/eslintrc": "npm:^3.1.0" + "@types/chai": "npm:^4.3.16" + "@types/mocha": "npm:^10.0.7" + "@types/node": "npm:^20.14.9" + "@typescript-eslint/parser": "npm:^7.17.0" + "@typescript-eslint/rule-tester": "npm:^7.17.0" + "@typescript-eslint/utils": "npm:^7.17.0" + chai: "npm:^5.1.1" + eslint: "npm:^8.56.0 <9.0.0" + mocha: "npm:^10.6.0" + ts-node: "npm:^10.9.2" + tsutils: "npm:^3.21.0" + tsx: "npm:^4.16.2" + typescript: "npm:^5.4.3 <5.5.0" + typescript-eslint: "npm:^7.17.0" + languageName: unknown + linkType: soft + +"eslint-scope@npm:^7.2.2": + version: 7.2.2 + resolution: "eslint-scope@npm:7.2.2" + dependencies: + esrecurse: "npm:^4.3.0" + estraverse: "npm:^5.2.0" + checksum: 10c0/613c267aea34b5a6d6c00514e8545ef1f1433108097e857225fed40d397dd6b1809dffd11c2fde23b37ca53d7bf935fe04d2a18e6fc932b31837b6ad67e1c116 + languageName: node + linkType: hard + +"eslint-scope@npm:^8.0.1": + version: 8.0.1 + resolution: "eslint-scope@npm:8.0.1" + dependencies: + esrecurse: "npm:^4.3.0" + estraverse: "npm:^5.2.0" + checksum: 10c0/0ec40ab284e58ac7ef064ecd23c127e03d339fa57173c96852336c73afc70ce5631da21dc1c772415a37a421291845538dd69db83c68d611044c0fde1d1fa269 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": + version: 3.4.3 + resolution: "eslint-visitor-keys@npm:3.4.3" + checksum: 10c0/92708e882c0a5ffd88c23c0b404ac1628cf20104a108c745f240a13c332a11aac54f49a22d5762efbffc18ecbc9a580d1b7ad034bf5f3cc3307e5cbff2ec9820 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^4.0.0": + version: 4.0.0 + resolution: "eslint-visitor-keys@npm:4.0.0" + checksum: 10c0/76619f42cf162705a1515a6868e6fc7567e185c7063a05621a8ac4c3b850d022661262c21d9f1fc1d144ecf0d5d64d70a3f43c15c3fc969a61ace0fb25698cf5 + languageName: node + linkType: hard + +"eslint@npm:^8.0.0, eslint@npm:^8.56.0 <9.0.0": + version: 8.57.0 + resolution: "eslint@npm:8.57.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@eslint-community/regexpp": "npm:^4.6.1" + "@eslint/eslintrc": "npm:^2.1.4" + "@eslint/js": "npm:8.57.0" + "@humanwhocodes/config-array": "npm:^0.11.14" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@nodelib/fs.walk": "npm:^1.2.8" + "@ungap/structured-clone": "npm:^1.2.0" + ajv: "npm:^6.12.4" + chalk: "npm:^4.0.0" + cross-spawn: "npm:^7.0.2" + debug: "npm:^4.3.2" + doctrine: "npm:^3.0.0" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^7.2.2" + eslint-visitor-keys: "npm:^3.4.3" + espree: "npm:^9.6.1" + esquery: "npm:^1.4.2" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^6.0.1" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + globals: "npm:^13.19.0" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + is-path-inside: "npm:^3.0.3" + js-yaml: "npm:^4.1.0" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + levn: "npm:^0.4.1" + lodash.merge: "npm:^4.6.2" + minimatch: "npm:^3.1.2" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + strip-ansi: "npm:^6.0.1" + text-table: "npm:^0.2.0" + bin: + eslint: bin/eslint.js + checksum: 10c0/00bb96fd2471039a312435a6776fe1fd557c056755eaa2b96093ef3a8508c92c8775d5f754768be6b1dddd09fdd3379ddb231eeb9b6c579ee17ea7d68000a529 + languageName: node + linkType: hard + +"eslint@npm:^9.6.0": + version: 9.6.0 + resolution: "eslint@npm:9.6.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@eslint-community/regexpp": "npm:^4.6.1" + "@eslint/config-array": "npm:^0.17.0" + "@eslint/eslintrc": "npm:^3.1.0" + "@eslint/js": "npm:9.6.0" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@humanwhocodes/retry": "npm:^0.3.0" + "@nodelib/fs.walk": "npm:^1.2.8" + ajv: "npm:^6.12.4" + chalk: "npm:^4.0.0" + cross-spawn: "npm:^7.0.2" + debug: "npm:^4.3.2" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^8.0.1" + eslint-visitor-keys: "npm:^4.0.0" + espree: "npm:^10.1.0" + esquery: "npm:^1.5.0" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^8.0.0" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + is-path-inside: "npm:^3.0.3" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + levn: "npm:^0.4.1" + lodash.merge: "npm:^4.6.2" + minimatch: "npm:^3.1.2" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + strip-ansi: "npm:^6.0.1" + text-table: "npm:^0.2.0" + bin: + eslint: bin/eslint.js + checksum: 10c0/82ea5ad3f28aaef89e2a98f4e6df0eae9d4e16ccd6d667c69977042e0b103fa5df98bf16d3df72d1ae77edd8c1dccfdf4afa2a55309aa8081a1bc54af6229826 + languageName: node + linkType: hard + +"espree@npm:^10.0.1, espree@npm:^10.1.0": + version: 10.1.0 + resolution: "espree@npm:10.1.0" + dependencies: + acorn: "npm:^8.12.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^4.0.0" + checksum: 10c0/52e6feaa77a31a6038f0c0e3fce93010a4625701925b0715cd54a2ae190b3275053a0717db698697b32653788ac04845e489d6773b508d6c2e8752f3c57470a0 + languageName: node + linkType: hard + +"espree@npm:^9.6.0, espree@npm:^9.6.1": + version: 9.6.1 + resolution: "espree@npm:9.6.1" + dependencies: + acorn: "npm:^8.9.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^3.4.1" + checksum: 10c0/1a2e9b4699b715347f62330bcc76aee224390c28bb02b31a3752e9d07549c473f5f986720483c6469cf3cfb3c9d05df612ffc69eb1ee94b54b739e67de9bb460 + languageName: node + linkType: hard + +"esquery@npm:^1.4.2, esquery@npm:^1.5.0": + version: 1.5.0 + resolution: "esquery@npm:1.5.0" + dependencies: + estraverse: "npm:^5.1.0" + checksum: 10c0/a084bd049d954cc88ac69df30534043fb2aee5555b56246493f42f27d1e168f00d9e5d4192e46f10290d312dc30dc7d58994d61a609c579c1219d636996f9213 + languageName: node + linkType: hard + +"esrecurse@npm:^4.3.0": + version: 4.3.0 + resolution: "esrecurse@npm:4.3.0" + dependencies: + estraverse: "npm:^5.2.0" + checksum: 10c0/81a37116d1408ded88ada45b9fb16dbd26fba3aadc369ce50fcaf82a0bac12772ebd7b24cd7b91fc66786bf2c1ac7b5f196bc990a473efff972f5cb338877cf5 + languageName: node + linkType: hard + +"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0": + version: 5.3.0 + resolution: "estraverse@npm:5.3.0" + checksum: 10c0/1ff9447b96263dec95d6d67431c5e0771eb9776427421260a3e2f0fdd5d6bd4f8e37a7338f5ad2880c9f143450c9b1e4fc2069060724570a49cf9cf0312bd107 + languageName: node + linkType: hard + +"esutils@npm:^2.0.2": + version: 2.0.3 + resolution: "esutils@npm:2.0.3" + checksum: 10c0/9a2fe69a41bfdade834ba7c42de4723c97ec776e40656919c62cbd13607c45e127a003f05f724a1ea55e5029a4cf2de444b13009f2af71271e42d93a637137c7 + languageName: node + linkType: hard + +"expect-violations@workspace:^, expect-violations@workspace:test-helpers/expect-violations": + version: 0.0.0-use.local + resolution: "expect-violations@workspace:test-helpers/expect-violations" + dependencies: + "@types/yargs": "npm:^17.0.32" + chai: "npm:^5.1.1" + yargs: "npm:^17.7.2" + bin: + expect-violations: ./bin/index.js + languageName: unknown + linkType: soft + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 10c0/160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 + languageName: node + linkType: hard + +"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": + version: 3.1.3 + resolution: "fast-deep-equal@npm:3.1.3" + checksum: 10c0/40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0 + languageName: node + linkType: hard + +"fast-glob@npm:^3.2.9": + version: 3.3.2 + resolution: "fast-glob@npm:3.3.2" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.4" + checksum: 10c0/42baad7b9cd40b63e42039132bde27ca2cb3a4950d0a0f9abe4639ea1aa9d3e3b40f98b1fe31cbc0cc17b664c9ea7447d911a152fa34ec5b72977b125a6fc845 + languageName: node + linkType: hard + +"fast-json-stable-stringify@npm:^2.0.0": + version: 2.1.0 + resolution: "fast-json-stable-stringify@npm:2.1.0" + checksum: 10c0/7f081eb0b8a64e0057b3bb03f974b3ef00135fbf36c1c710895cd9300f13c94ba809bb3a81cf4e1b03f6e5285610a61abbd7602d0652de423144dfee5a389c9b + languageName: node + linkType: hard + +"fast-levenshtein@npm:^2.0.6": + version: 2.0.6 + resolution: "fast-levenshtein@npm:2.0.6" + checksum: 10c0/111972b37338bcb88f7d9e2c5907862c280ebf4234433b95bc611e518d192ccb2d38119c4ac86e26b668d75f7f3894f4ff5c4982899afced7ca78633b08287c4 + languageName: node + linkType: hard + +"fastq@npm:^1.6.0": + version: 1.17.1 + resolution: "fastq@npm:1.17.1" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10c0/1095f16cea45fb3beff558bb3afa74ca7a9250f5a670b65db7ed585f92b4b48381445cd328b3d87323da81e43232b5d5978a8201bde84e0cd514310f1ea6da34 + languageName: node + linkType: hard + +"file-entry-cache@npm:^6.0.1": + version: 6.0.1 + resolution: "file-entry-cache@npm:6.0.1" + dependencies: + flat-cache: "npm:^3.0.4" + checksum: 10c0/58473e8a82794d01b38e5e435f6feaf648e3f36fdb3a56e98f417f4efae71ad1c0d4ebd8a9a7c50c3ad085820a93fc7494ad721e0e4ebc1da3573f4e1c3c7cdd + languageName: node + linkType: hard + +"file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" + dependencies: + flat-cache: "npm:^4.0.0" + checksum: 10c0/9e2b5938b1cd9b6d7e3612bdc533afd4ac17b2fc646569e9a8abbf2eb48e5eb8e316bc38815a3ef6a1b456f4107f0d0f055a614ca613e75db6bf9ff4d72c1638 + languageName: node + linkType: hard + +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018 + languageName: node + linkType: hard + +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: "npm:^6.0.0" + path-exists: "npm:^4.0.0" + checksum: 10c0/062c5a83a9c02f53cdd6d175a37ecf8f87ea5bbff1fdfb828f04bfa021441bc7583e8ebc0872a4c1baab96221fb8a8a275a19809fb93fbc40bd69ec35634069a + languageName: node + linkType: hard + +"flat-cache@npm:^3.0.4": + version: 3.2.0 + resolution: "flat-cache@npm:3.2.0" + dependencies: + flatted: "npm:^3.2.9" + keyv: "npm:^4.5.3" + rimraf: "npm:^3.0.2" + checksum: 10c0/b76f611bd5f5d68f7ae632e3ae503e678d205cf97a17c6ab5b12f6ca61188b5f1f7464503efae6dc18683ed8f0b41460beb48ac4b9ac63fe6201296a91ba2f75 + languageName: node + linkType: hard + +"flat-cache@npm:^4.0.0": + version: 4.0.1 + resolution: "flat-cache@npm:4.0.1" + dependencies: + flatted: "npm:^3.2.9" + keyv: "npm:^4.5.4" + checksum: 10c0/2c59d93e9faa2523e4fda6b4ada749bed432cfa28c8e251f33b25795e426a1c6dbada777afb1f74fcfff33934fdbdea921ee738fcc33e71adc9d6eca984a1cfc + languageName: node + linkType: hard + +"flat@npm:^5.0.2": + version: 5.0.2 + resolution: "flat@npm:5.0.2" + bin: + flat: cli.js + checksum: 10c0/f178b13482f0cd80c7fede05f4d10585b1f2fdebf26e12edc138e32d3150c6ea6482b7f12813a1091143bad52bb6d3596bca51a162257a21163c0ff438baa5fe + languageName: node + linkType: hard + +"flatted@npm:^3.2.9": + version: 3.3.1 + resolution: "flatted@npm:3.3.1" + checksum: 10c0/324166b125ee07d4ca9bcf3a5f98d915d5db4f39d711fba640a3178b959919aae1f7cfd8aabcfef5826ed8aa8a2aa14cc85b2d7d18ff638ddf4ae3df39573eaf + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.2.1 + resolution: "foreground-child@npm:3.2.1" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 10c0/9a53a33dbd87090e9576bef65fb4a71de60f6863a8062a7b11bc1cbe3cc86d428677d7c0b9ef61cdac11007ac580006f78bd5638618d564cfd5e6fd713d6878f + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fs.realpath@npm:^1.0.0": + version: 1.0.0 + resolution: "fs.realpath@npm:1.0.0" + checksum: 10c0/444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 + languageName: node + linkType: hard + +"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde + languageName: node + linkType: hard + +"get-func-name@npm:^2.0.1": + version: 2.0.2 + resolution: "get-func-name@npm:2.0.2" + checksum: 10c0/89830fd07623fa73429a711b9daecdb304386d237c71268007f788f113505ef1d4cc2d0b9680e072c5082490aec9df5d7758bf5ac6f1c37062855e8e3dc0b9df + languageName: node + linkType: hard + +"get-tsconfig@npm:^4.7.5": + version: 4.7.5 + resolution: "get-tsconfig@npm:4.7.5" + dependencies: + resolve-pkg-maps: "npm:^1.0.0" + checksum: 10c0/a917dff2ba9ee187c41945736bf9bbab65de31ce5bc1effd76267be483a7340915cff232199406379f26517d2d0a4edcdbcda8cca599c2480a0f2cf1e1de3efa + languageName: node + linkType: hard + +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee + languageName: node + linkType: hard + +"glob-parent@npm:^6.0.2": + version: 6.0.2 + resolution: "glob-parent@npm:6.0.2" + dependencies: + is-glob: "npm:^4.0.3" + checksum: 10c0/317034d88654730230b3f43bb7ad4f7c90257a426e872ea0bf157473ac61c99bf5d205fad8f0185f989be8d2fa6d3c7dce1645d99d545b6ea9089c39f838e7f8 + languageName: node + linkType: hard + +"glob@npm:^10.2.2, glob@npm:^10.3.10": + version: 10.4.5 + resolution: "glob@npm:10.4.5" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e + languageName: node + linkType: hard + +"glob@npm:^7.1.3": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^3.1.1" + once: "npm:^1.3.0" + path-is-absolute: "npm:^1.0.0" + checksum: 10c0/65676153e2b0c9095100fe7f25a778bf45608eeb32c6048cf307f579649bcc30353277b3b898a3792602c65764e5baa4f643714dfbdfd64ea271d210c7a425fe + languageName: node + linkType: hard + +"glob@npm:^8.1.0": + version: 8.1.0 + resolution: "glob@npm:8.1.0" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^5.0.1" + once: "npm:^1.3.0" + checksum: 10c0/cb0b5cab17a59c57299376abe5646c7070f8acb89df5595b492dba3bfb43d301a46c01e5695f01154e6553168207cb60d4eaf07d3be4bc3eb9b0457c5c561d0f + languageName: node + linkType: hard + +"globals@npm:^13.19.0": + version: 13.24.0 + resolution: "globals@npm:13.24.0" + dependencies: + type-fest: "npm:^0.20.2" + checksum: 10c0/d3c11aeea898eb83d5ec7a99508600fbe8f83d2cf00cbb77f873dbf2bcb39428eff1b538e4915c993d8a3b3473fa71eeebfe22c9bb3a3003d1e26b1f2c8a42cd + languageName: node + linkType: hard + +"globals@npm:^14.0.0": + version: 14.0.0 + resolution: "globals@npm:14.0.0" + checksum: 10c0/b96ff42620c9231ad468d4c58ff42afee7777ee1c963013ff8aabe095a451d0ceeb8dcd8ef4cbd64d2538cef45f787a78ba3a9574f4a634438963e334471302d + languageName: node + linkType: hard + +"globby@npm:^11.1.0": + version: 11.1.0 + resolution: "globby@npm:11.1.0" + dependencies: + array-union: "npm:^2.1.0" + dir-glob: "npm:^3.0.1" + fast-glob: "npm:^3.2.9" + ignore: "npm:^5.2.0" + merge2: "npm:^1.4.1" + slash: "npm:^3.0.0" + checksum: 10c0/b39511b4afe4bd8a7aead3a27c4ade2b9968649abab0a6c28b1a90141b96ca68ca5db1302f7c7bd29eab66bf51e13916b8e0a3d0ac08f75e1e84a39b35691189 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"graphemer@npm:^1.4.0": + version: 1.4.0 + resolution: "graphemer@npm:1.4.0" + checksum: 10c0/e951259d8cd2e0d196c72ec711add7115d42eb9a8146c8eeda5b8d3ac91e5dd816b9cd68920726d9fd4490368e7ed86e9c423f40db87e2d8dfafa00fa17c3a31 + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 10c0/2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1 + languageName: node + linkType: hard + +"he@npm:^1.2.0": + version: 1.2.0 + resolution: "he@npm:1.2.0" + bin: + he: bin/he + checksum: 10c0/a27d478befe3c8192f006cdd0639a66798979dfa6e2125c6ac582a19a5ebfec62ad83e8382e6036170d873f46e4536a7e795bf8b95bf7c247f4cc0825ccc8c17 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.5 + resolution: "https-proxy-agent@npm:7.0.5" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:4" + checksum: 10c0/2490e3acec397abeb88807db52cac59102d5ed758feee6df6112ab3ccd8325e8a1ce8bce6f4b66e5470eca102d31e425ace904242e4fa28dbe0c59c4bafa7b2c + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + +"ignore@npm:^5.2.0, ignore@npm:^5.3.1": + version: 5.3.1 + resolution: "ignore@npm:5.3.1" + checksum: 10c0/703f7f45ffb2a27fb2c5a8db0c32e7dee66b33a225d28e8db4e1be6474795f606686a6e3bcc50e1aa12f2042db4c9d4a7d60af3250511de74620fbed052ea4cd + languageName: node + linkType: hard + +"import-fresh@npm:^3.2.1": + version: 3.3.0 + resolution: "import-fresh@npm:3.3.0" + dependencies: + parent-module: "npm:^1.0.0" + resolve-from: "npm:^4.0.0" + checksum: 10c0/7f882953aa6b740d1f0e384d0547158bc86efbf2eea0f1483b8900a6f65c5a5123c2cf09b0d542cc419d0b98a759ecaeb394237e97ea427f2da221dc3cd80cc3 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + languageName: node + linkType: hard + +"inflight@npm:^1.0.4": + version: 1.0.6 + resolution: "inflight@npm:1.0.6" + dependencies: + once: "npm:^1.3.0" + wrappy: "npm:1" + checksum: 10c0/7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 + languageName: node + linkType: hard + +"inherits@npm:2": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 + languageName: node + linkType: hard + +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc + languageName: node + linkType: hard + +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + languageName: node + linkType: hard + +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 10c0/85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 + languageName: node + linkType: hard + +"is-path-inside@npm:^3.0.3": + version: 3.0.3 + resolution: "is-path-inside@npm:3.0.3" + checksum: 10c0/cf7d4ac35fb96bab6a1d2c3598fe5ebb29aafb52c0aaa482b5a3ed9d8ba3edc11631e3ec2637660c44b3ce0e61a08d54946e8af30dec0b60a7c27296c68ffd05 + languageName: node + linkType: hard + +"is-plain-obj@npm:^2.1.0": + version: 2.1.0 + resolution: "is-plain-obj@npm:2.1.0" + checksum: 10c0/e5c9814cdaa627a9ad0a0964ded0e0491bfd9ace405c49a5d63c88b30a162f1512c069d5b80997893c4d0181eadc3fed02b4ab4b81059aba5620bfcdfdeb9c53 + languageName: node + linkType: hard + +"is-unicode-supported@npm:^0.1.0": + version: 0.1.0 + resolution: "is-unicode-supported@npm:0.1.0" + checksum: 10c0/00cbe3455c3756be68d2542c416cab888aebd5012781d6819749fefb15162ff23e38501fe681b3d751c73e8ff561ac09a5293eba6f58fdf0178462ce6dcb3453 + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + +"jackspeak@npm:^3.1.2": + version: 3.4.3 + resolution: "jackspeak@npm:3.4.3" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 10c0/6acc10d139eaefdbe04d2f679e6191b3abf073f111edf10b1de5302c97ec93fffeb2fdd8681ed17f16268aa9dd4f8c588ed9d1d3bffbbfa6e8bf897cbb3149b9 + languageName: node + linkType: hard + +"js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10c0/184a24b4eaacfce40ad9074c64fd42ac83cf74d8c8cd137718d456ced75051229e5061b8633c3366b8aada17945a7a356b337828c19da92b51ae62126575018f + languageName: node + linkType: hard + +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 + languageName: node + linkType: hard + +"json-buffer@npm:3.0.1": + version: 3.0.1 + resolution: "json-buffer@npm:3.0.1" + checksum: 10c0/0d1c91569d9588e7eef2b49b59851f297f3ab93c7b35c7c221e288099322be6b562767d11e4821da500f3219542b9afd2e54c5dc573107c1126ed1080f8e96d7 + languageName: node + linkType: hard + +"json-schema-traverse@npm:^0.4.1": + version: 0.4.1 + resolution: "json-schema-traverse@npm:0.4.1" + checksum: 10c0/108fa90d4cc6f08243aedc6da16c408daf81793bf903e9fd5ab21983cda433d5d2da49e40711da016289465ec2e62e0324dcdfbc06275a607fe3233fde4942ce + languageName: node + linkType: hard + +"json-stable-stringify-without-jsonify@npm:^1.0.1": + version: 1.0.1 + resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" + checksum: 10c0/cb168b61fd4de83e58d09aaa6425ef71001bae30d260e2c57e7d09a5fd82223e2f22a042dedaab8db23b7d9ae46854b08bb1f91675a8be11c5cffebef5fb66a5 + languageName: node + linkType: hard + +"keyv@npm:^4.5.3, keyv@npm:^4.5.4": + version: 4.5.4 + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: "npm:3.0.1" + checksum: 10c0/aa52f3c5e18e16bb6324876bb8b59dd02acf782a4b789c7b2ae21107fab95fab3890ed448d4f8dba80ce05391eeac4bfabb4f02a20221342982f806fa2cf271e + languageName: node + linkType: hard + +"levn@npm:^0.4.1": + version: 0.4.1 + resolution: "levn@npm:0.4.1" + dependencies: + prelude-ls: "npm:^1.2.1" + type-check: "npm:~0.4.0" + checksum: 10c0/effb03cad7c89dfa5bd4f6989364bfc79994c2042ec5966cb9b95990e2edee5cd8969ddf42616a0373ac49fac1403437deaf6e9050fbbaa3546093a59b9ac94e + languageName: node + linkType: hard + +"locate-path@npm:^6.0.0": + version: 6.0.0 + resolution: "locate-path@npm:6.0.0" + dependencies: + p-locate: "npm:^5.0.0" + checksum: 10c0/d3972ab70dfe58ce620e64265f90162d247e87159b6126b01314dd67be43d50e96a50b517bce2d9452a79409c7614054c277b5232377de50416564a77ac7aad3 + languageName: node + linkType: hard + +"lodash.merge@npm:4.6.2, lodash.merge@npm:^4.6.2": + version: 4.6.2 + resolution: "lodash.merge@npm:4.6.2" + checksum: 10c0/402fa16a1edd7538de5b5903a90228aa48eb5533986ba7fa26606a49db2572bf414ff73a2c9f5d5fd36b31c46a5d5c7e1527749c07cbcf965ccff5fbdf32c506 + languageName: node + linkType: hard + +"log-symbols@npm:^4.1.0": + version: 4.1.0 + resolution: "log-symbols@npm:4.1.0" + dependencies: + chalk: "npm:^4.1.0" + is-unicode-supported: "npm:^0.1.0" + checksum: 10c0/67f445a9ffa76db1989d0fa98586e5bc2fd5247260dafb8ad93d9f0ccd5896d53fb830b0e54dade5ad838b9de2006c826831a3c528913093af20dff8bd24aca6 + languageName: node + linkType: hard + +"loupe@npm:^3.1.0": + version: 3.1.1 + resolution: "loupe@npm:3.1.1" + dependencies: + get-func-name: "npm:^2.0.1" + checksum: 10c0/99f88badc47e894016df0c403de846fedfea61154aadabbf776c8428dd59e8d8378007135d385d737de32ae47980af07d22ba7bec5ef7beebd721de9baa0a0af + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.4.3 + resolution: "lru-cache@npm:10.4.3" + checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb + languageName: node + linkType: hard + +"make-error@npm:^1.1.1": + version: 1.3.6 + resolution: "make-error@npm:1.3.6" + checksum: 10c0/171e458d86854c6b3fc46610cfacf0b45149ba043782558c6875d9f42f222124384ad0b468c92e996d815a8a2003817a710c0a160e49c1c394626f76fa45396f + languageName: node + linkType: hard + +"make-fetch-happen@npm:^13.0.0": + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" + dependencies: + "@npmcli/agent": "npm:^2.0.0" + cacache: "npm:^18.0.0" + http-cache-semantics: "npm:^4.1.1" + is-lambda: "npm:^1.0.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" + promise-retry: "npm:^2.0.1" + ssri: "npm:^10.0.0" + checksum: 10c0/df5f4dbb6d98153b751bccf4dc4cc500de85a96a9331db9805596c46aa9f99d9555983954e6c1266d9f981ae37a9e4647f42b9a4bb5466f867f4012e582c9e7e + languageName: node + linkType: hard + +"merge2@npm:^1.3.0, merge2@npm:^1.4.1": + version: 1.4.1 + resolution: "merge2@npm:1.4.1" + checksum: 10c0/254a8a4605b58f450308fc474c82ac9a094848081bf4c06778200207820e5193726dc563a0d2c16468810516a5c97d9d3ea0ca6585d23c58ccfff2403e8dbbeb + languageName: node + linkType: hard + +"micromatch@npm:^4.0.4": + version: 4.0.7 + resolution: "micromatch@npm:4.0.7" + dependencies: + braces: "npm:^3.0.3" + picomatch: "npm:^2.3.1" + checksum: 10c0/58fa99bc5265edec206e9163a1d2cec5fabc46a5b473c45f4a700adce88c2520456ae35f2b301e4410fb3afb27e9521fb2813f6fc96be0a48a89430e0916a772 + languageName: node + linkType: hard + +"minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + languageName: node + linkType: hard + +"minimatch@npm:^5.0.1, minimatch@npm:^5.1.6": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10c0/3defdfd230914f22a8da203747c42ee3c405c39d4d37ffda284dac5e45b7e1f6c49aa8be606509002898e73091ff2a3bbfc59c2c6c71d4660609f63aa92f98e3 + languageName: node + linkType: hard + +"minimatch@npm:^9.0.4": + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10c0/de96cf5e35bdf0eab3e2c853522f98ffbe9a36c37797778d2665231ec1f20a9447a7e567cb640901f89e4daaa95ae5d70c65a9e8aa2bb0019b6facbc3c0575ed + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: 10c0/9d702d57f556274286fdd97e406fc38a2f5c8d15e158b498d7393b1105974b21249289ec571fa2b51e038a4872bfc82710111cf75fae98c662f3d6f95e72152b + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 10c0/a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.1.2": + version: 7.1.2 + resolution: "minipass@npm:7.1.2" + checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: 10c0/64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: 10c0/46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + languageName: node + linkType: hard + +"mocha@npm:^10.6.0": + version: 10.6.0 + resolution: "mocha@npm:10.6.0" + dependencies: + ansi-colors: "npm:^4.1.3" + browser-stdout: "npm:^1.3.1" + chokidar: "npm:^3.5.3" + debug: "npm:^4.3.5" + diff: "npm:^5.2.0" + escape-string-regexp: "npm:^4.0.0" + find-up: "npm:^5.0.0" + glob: "npm:^8.1.0" + he: "npm:^1.2.0" + js-yaml: "npm:^4.1.0" + log-symbols: "npm:^4.1.0" + minimatch: "npm:^5.1.6" + ms: "npm:^2.1.3" + serialize-javascript: "npm:^6.0.2" + strip-json-comments: "npm:^3.1.1" + supports-color: "npm:^8.1.1" + workerpool: "npm:^6.5.1" + yargs: "npm:^16.2.0" + yargs-parser: "npm:^20.2.9" + yargs-unparser: "npm:^2.0.0" + bin: + _mocha: bin/_mocha + mocha: bin/mocha.js + checksum: 10c0/30b2f810014af6b5701563c6ee6ee78708dcfefc1551801c70018682bc6ca9327a6a27e93c101905a355d130a1ffe1f990975d51459c289bfcb72726ea5f7a50 + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: 10c0/a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc + languageName: node + linkType: hard + +"ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + +"natural-compare@npm:^1.4.0": + version: 1.4.0 + resolution: "natural-compare@npm:1.4.0" + checksum: 10c0/f5f9a7974bfb28a91afafa254b197f0f22c684d4a1731763dda960d2c8e375b36c7d690e0d9dc8fba774c537af14a7e979129bca23d88d052fbeb9466955e447 + languageName: node + linkType: hard + +"negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: 10c0/3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 10.2.0 + resolution: "node-gyp@npm:10.2.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^13.0.0" + nopt: "npm:^7.0.0" + proc-log: "npm:^4.1.0" + semver: "npm:^7.3.5" + tar: "npm:^6.2.1" + which: "npm:^4.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/00630d67dbd09a45aee0a5d55c05e3916ca9e6d427ee4f7bc392d2d3dc5fad7449b21fc098dd38260a53d9dcc9c879b36704a1994235d4707e7271af7e9a835b + languageName: node + linkType: hard + +"nopt@npm:^7.0.0": + version: 7.2.1 + resolution: "nopt@npm:7.2.1" + dependencies: + abbrev: "npm:^2.0.0" + bin: + nopt: bin/nopt.js + checksum: 10c0/a069c7c736767121242037a22a788863accfa932ab285a1eb569eb8cd534b09d17206f68c37f096ae785647435e0c5a5a0a67b42ec743e481a455e5ae6a6df81 + languageName: node + linkType: hard + +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 + languageName: node + linkType: hard + +"once@npm:^1.3.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 + languageName: node + linkType: hard + +"optionator@npm:^0.9.3": + version: 0.9.4 + resolution: "optionator@npm:0.9.4" + dependencies: + deep-is: "npm:^0.1.3" + fast-levenshtein: "npm:^2.0.6" + levn: "npm:^0.4.1" + prelude-ls: "npm:^1.2.1" + type-check: "npm:^0.4.0" + word-wrap: "npm:^1.2.5" + checksum: 10c0/4afb687a059ee65b61df74dfe87d8d6815cd6883cb8b3d5883a910df72d0f5d029821f37025e4bccf4048873dbdb09acc6d303d27b8f76b1a80dd5a7d5334675 + languageName: node + linkType: hard + +"p-limit@npm:^3.0.2": + version: 3.1.0 + resolution: "p-limit@npm:3.1.0" + dependencies: + yocto-queue: "npm:^0.1.0" + checksum: 10c0/9db675949dbdc9c3763c89e748d0ef8bdad0afbb24d49ceaf4c46c02c77d30db4e0652ed36d0a0a7a95154335fab810d95c86153105bb73b3a90448e2bb14e1a + languageName: node + linkType: hard + +"p-locate@npm:^5.0.0": + version: 5.0.0 + resolution: "p-locate@npm:5.0.0" + dependencies: + p-limit: "npm:^3.0.2" + checksum: 10c0/2290d627ab7903b8b70d11d384fee714b797f6040d9278932754a6860845c4d3190603a0772a663c8cb5a7b21d1b16acb3a6487ebcafa9773094edc3dfe6009a + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 10c0/592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 + languageName: node + linkType: hard + +"package-json-from-dist@npm:^1.0.0": + version: 1.0.0 + resolution: "package-json-from-dist@npm:1.0.0" + checksum: 10c0/e3ffaf6ac1040ab6082a658230c041ad14e72fabe99076a2081bb1d5d41210f11872403fc09082daf4387fc0baa6577f96c9c0e94c90c394fd57794b66aa4033 + languageName: node + linkType: hard + +"parent-module@npm:^1.0.0": + version: 1.0.1 + resolution: "parent-module@npm:1.0.1" + dependencies: + callsites: "npm:^3.0.0" + checksum: 10c0/c63d6e80000d4babd11978e0d3fee386ca7752a02b035fd2435960ffaa7219dc42146f07069fb65e6e8bf1caef89daf9af7535a39bddf354d78bf50d8294f556 + languageName: node + linkType: hard + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 10c0/8c0bd3f5238188197dc78dced15207a4716c51cc4e3624c44fc97acf69558f5ebb9a2afff486fe1b4ee148e0c133e96c5e11a9aa5c48a3006e3467da070e5e1b + languageName: node + linkType: hard + +"path-is-absolute@npm:^1.0.0": + version: 1.0.1 + resolution: "path-is-absolute@npm:1.0.1" + checksum: 10c0/127da03c82172a2a50099cddbf02510c1791fc2cc5f7713ddb613a56838db1e8168b121a920079d052e0936c23005562059756d653b7c544c53185efe53be078 + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-scurry@npm:^1.11.1": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" + dependencies: + lru-cache: "npm:^10.2.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d + languageName: node + linkType: hard + +"path-type@npm:^4.0.0": + version: 4.0.0 + resolution: "path-type@npm:4.0.0" + checksum: 10c0/666f6973f332f27581371efaf303fd6c272cc43c2057b37aa99e3643158c7e4b2626549555d88626e99ea9e046f82f32e41bbde5f1508547e9a11b149b52387c + languageName: node + linkType: hard + +"pathval@npm:^2.0.0": + version: 2.0.0 + resolution: "pathval@npm:2.0.0" + checksum: 10c0/602e4ee347fba8a599115af2ccd8179836a63c925c23e04bd056d0674a64b39e3a081b643cc7bc0b84390517df2d800a46fcc5598d42c155fe4977095c2f77c5 + languageName: node + linkType: hard + +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be + languageName: node + linkType: hard + +"prelude-ls@npm:^1.2.1": + version: 1.2.1 + resolution: "prelude-ls@npm:1.2.1" + checksum: 10c0/b00d617431e7886c520a6f498a2e14c75ec58f6d93ba48c3b639cf241b54232d90daa05d83a9e9b9fef6baa63cb7e1e4602c2372fea5bc169668401eb127d0cd + languageName: node + linkType: hard + +"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 10c0/17db4757c2a5c44c1e545170e6c70a26f7de58feb985091fb1763f5081cab3d01b181fb2dd240c9f4a4255a1d9227d163d5771b7e69c9e49a561692db865efb9 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + languageName: node + linkType: hard + +"punycode@npm:^2.1.0": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 + languageName: node + linkType: hard + +"queue-microtask@npm:^1.2.2": + version: 1.2.3 + resolution: "queue-microtask@npm:1.2.3" + checksum: 10c0/900a93d3cdae3acd7d16f642c29a642aea32c2026446151f0778c62ac089d4b8e6c986811076e1ae180a694cedf077d453a11b58ff0a865629a4f82ab558e102 + languageName: node + linkType: hard + +"randombytes@npm:^2.1.0": + version: 2.1.0 + resolution: "randombytes@npm:2.1.0" + dependencies: + safe-buffer: "npm:^5.1.0" + checksum: 10c0/50395efda7a8c94f5dffab564f9ff89736064d32addf0cc7e8bf5e4166f09f8ded7a0849ca6c2d2a59478f7d90f78f20d8048bca3cdf8be09d8e8a10790388f3 + languageName: node + linkType: hard + +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99 + languageName: node + linkType: hard + +"resolve-from@npm:^4.0.0": + version: 4.0.0 + resolution: "resolve-from@npm:4.0.0" + checksum: 10c0/8408eec31a3112ef96e3746c37be7d64020cda07c03a920f5024e77290a218ea758b26ca9529fd7b1ad283947f34b2291c1c0f6aa0ed34acfdda9c6014c8d190 + languageName: node + linkType: hard + +"resolve-pkg-maps@npm:^1.0.0": + version: 1.0.0 + resolution: "resolve-pkg-maps@npm:1.0.0" + checksum: 10c0/fb8f7bbe2ca281a73b7ef423a1cbc786fb244bd7a95cbe5c3fba25b27d327150beca8ba02f622baea65919a57e061eb5005204daa5f93ed590d9b77463a567ab + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + +"reusify@npm:^1.0.4": + version: 1.0.4 + resolution: "reusify@npm:1.0.4" + checksum: 10c0/c19ef26e4e188f408922c46f7ff480d38e8dfc55d448310dfb518736b23ed2c4f547fb64a6ed5bdba92cd7e7ddc889d36ff78f794816d5e71498d645ef476107 + languageName: node + linkType: hard + +"rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: bin.js + checksum: 10c0/9cb7757acb489bd83757ba1a274ab545eafd75598a9d817e0c3f8b164238dd90eba50d6b848bd4dcc5f3040912e882dc7ba71653e35af660d77b25c381d402e8 + languageName: node + linkType: hard + +"root-workspace-0b6124@workspace:.": + version: 0.0.0-use.local + resolution: "root-workspace-0b6124@workspace:." + languageName: unknown + linkType: soft + +"run-parallel@npm:^1.1.9": + version: 1.2.0 + resolution: "run-parallel@npm:1.2.0" + dependencies: + queue-microtask: "npm:^1.2.2" + checksum: 10c0/200b5ab25b5b8b7113f9901bfe3afc347e19bb7475b267d55ad0eb86a62a46d77510cb0f232507c9e5d497ebda569a08a9867d0d14f57a82ad5564d991588b39 + languageName: node + linkType: hard + +"safe-buffer@npm:^5.1.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + +"semver@npm:^7.3.5": + version: 7.6.3 + resolution: "semver@npm:7.6.3" + bin: + semver: bin/semver.js + checksum: 10c0/88f33e148b210c153873cb08cfe1e281d518aaa9a666d4d148add6560db5cd3c582f3a08ccb91f38d5f379ead256da9931234ed122057f40bb5766e65e58adaf + languageName: node + linkType: hard + +"semver@npm:^7.6.0": + version: 7.6.2 + resolution: "semver@npm:7.6.2" + bin: + semver: bin/semver.js + checksum: 10c0/97d3441e97ace8be4b1976433d1c32658f6afaff09f143e52c593bae7eef33de19e3e369c88bd985ce1042c6f441c80c6803078d1de2a9988080b66684cbb30c + languageName: node + linkType: hard + +"serialize-javascript@npm:^6.0.2": + version: 6.0.2 + resolution: "serialize-javascript@npm:6.0.2" + dependencies: + randombytes: "npm:^2.1.0" + checksum: 10c0/2dd09ef4b65a1289ba24a788b1423a035581bef60817bea1f01eda8e3bda623f86357665fe7ac1b50f6d4f583f97db9615b3f07b2a2e8cbcb75033965f771dd2 + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"slash@npm:^3.0.0": + version: 3.0.0 + resolution: "slash@npm:3.0.0" + checksum: 10c0/e18488c6a42bdfd4ac5be85b2ced3ccd0224773baae6ad42cfbb9ec74fc07f9fa8396bd35ee638084ead7a2a0818eb5e7151111544d4731ce843019dab4be47b + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.4 + resolution: "socks-proxy-agent@npm:8.0.4" + dependencies: + agent-base: "npm:^7.1.1" + debug: "npm:^4.3.4" + socks: "npm:^2.8.3" + checksum: 10c0/345593bb21b95b0508e63e703c84da11549f0a2657d6b4e3ee3612c312cb3a907eac10e53b23ede3557c6601d63252103494caa306b66560f43af7b98f53957a + languageName: node + linkType: hard + +"socks@npm:^2.8.3": + version: 2.8.3 + resolution: "socks@npm:2.8.3" + dependencies: + ip-address: "npm:^9.0.5" + smart-buffer: "npm:^4.2.0" + checksum: 10c0/d54a52bf9325165770b674a67241143a3d8b4e4c8884560c4e0e078aace2a728dffc7f70150660f51b85797c4e1a3b82f9b7aa25e0a0ceae1a243365da5c51a7 + languageName: node + linkType: hard + +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.6 + resolution: "ssri@npm:10.0.6" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/e5a1e23a4057a86a97971465418f22ea89bd439ac36ade88812dd920e4e61873e8abd6a9b72a03a67ef50faa00a2daf1ab745c5a15b46d03e0544a0296354227 + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + languageName: node + linkType: hard + +"strip-json-comments@npm:^3.1.1": + version: 3.1.1 + resolution: "strip-json-comments@npm:3.1.1" + checksum: 10c0/9681a6257b925a7fa0f285851c0e613cc934a50661fa7bb41ca9cbbff89686bb4a0ee366e6ecedc4daafd01e83eee0720111ab294366fe7c185e935475ebcecd + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10c0/afb4c88521b8b136b5f5f95160c98dee7243dc79d5432db7efc27efb219385bbc7d9427398e43dd6cc730a0f87d5085ce1652af7efbe391327bc0a7d0f7fc124 + languageName: node + linkType: hard + +"supports-color@npm:^8.1.1": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89 + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.2.1": + version: 6.2.1 + resolution: "tar@npm:6.2.1" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 + languageName: node + linkType: hard + +"text-table@npm:^0.2.0": + version: 0.2.0 + resolution: "text-table@npm:0.2.0" + checksum: 10c0/02805740c12851ea5982686810702e2f14369a5f4c5c40a836821e3eefc65ffeec3131ba324692a37608294b0fd8c1e55a2dd571ffed4909822787668ddbee5c + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 + languageName: node + linkType: hard + +"ts-api-utils@npm:^1.3.0": + version: 1.3.0 + resolution: "ts-api-utils@npm:1.3.0" + peerDependencies: + typescript: ">=4.2.0" + checksum: 10c0/f54a0ba9ed56ce66baea90a3fa087a484002e807f28a8ccb2d070c75e76bde64bd0f6dce98b3802834156306050871b67eec325cb4e918015a360a3f0868c77c + languageName: node + linkType: hard + +"ts-node@npm:^10.9.2": + version: 10.9.2 + resolution: "ts-node@npm:10.9.2" + dependencies: + "@cspotcode/source-map-support": "npm:^0.8.0" + "@tsconfig/node10": "npm:^1.0.7" + "@tsconfig/node12": "npm:^1.0.7" + "@tsconfig/node14": "npm:^1.0.0" + "@tsconfig/node16": "npm:^1.0.2" + acorn: "npm:^8.4.1" + acorn-walk: "npm:^8.1.1" + arg: "npm:^4.1.0" + create-require: "npm:^1.1.0" + diff: "npm:^4.0.1" + make-error: "npm:^1.1.1" + v8-compile-cache-lib: "npm:^3.0.1" + yn: "npm:3.1.1" + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 10c0/5f29938489f96982a25ba650b64218e83a3357d76f7bede80195c65ab44ad279c8357264639b7abdd5d7e75fc269a83daa0e9c62fd8637a3def67254ecc9ddc2 + languageName: node + linkType: hard + +"tslib@npm:^1.8.1": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 + languageName: node + linkType: hard + +"tsutils@npm:^3.21.0": + version: 3.21.0 + resolution: "tsutils@npm:3.21.0" + dependencies: + tslib: "npm:^1.8.1" + 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" + checksum: 10c0/02f19e458ec78ead8fffbf711f834ad8ecd2cc6ade4ec0320790713dccc0a412b99e7fd907c4cda2a1dc602c75db6f12e0108e87a5afad4b2f9e90a24cabd5a2 + languageName: node + linkType: hard + +"tsx@npm:^4.16.2": + version: 4.16.2 + resolution: "tsx@npm:4.16.2" + dependencies: + esbuild: "npm:~0.21.5" + fsevents: "npm:~2.3.3" + get-tsconfig: "npm:^4.7.5" + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: 10c0/9df52264f88be00ca473e7d7eda43bb038cc09028514996b864db78645e9cd297c71485f0fdd4985464d6dc46424f8bef9f8c4bd56692c4fcf4d71621ae21763 + languageName: node + linkType: hard + +"type-check@npm:^0.4.0, type-check@npm:~0.4.0": + version: 0.4.0 + resolution: "type-check@npm:0.4.0" + dependencies: + prelude-ls: "npm:^1.2.1" + checksum: 10c0/7b3fd0ed43891e2080bf0c5c504b418fbb3e5c7b9708d3d015037ba2e6323a28152ec163bcb65212741fa5d2022e3075ac3c76440dbd344c9035f818e8ecee58 + languageName: node + linkType: hard + +"type-fest@npm:^0.20.2": + version: 0.20.2 + resolution: "type-fest@npm:0.20.2" + checksum: 10c0/dea9df45ea1f0aaa4e2d3bed3f9a0bfe9e5b2592bddb92eb1bf06e50bcf98dbb78189668cd8bc31a0511d3fc25539b4cd5c704497e53e93e2d40ca764b10bfc3 + languageName: node + linkType: hard + +"typescript-eslint@npm:^7.17.0": + version: 7.17.0 + resolution: "typescript-eslint@npm:7.17.0" + dependencies: + "@typescript-eslint/eslint-plugin": "npm:7.17.0" + "@typescript-eslint/parser": "npm:7.17.0" + "@typescript-eslint/utils": "npm:7.17.0" + peerDependencies: + eslint: ^8.56.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/5d9a5430b139129474cd65655ef05efeddfbd890a3ff28c1dd3b747cf48c938cf18e95b3cbbb4a7f8bf991c6c2de5e82f685768c2e7d691629a404a178672a13 + languageName: node + linkType: hard + +"typescript@npm:^5.4.3 <5.5.0": + version: 5.4.5 + resolution: "typescript@npm:5.4.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/2954022ada340fd3d6a9e2b8e534f65d57c92d5f3989a263754a78aba549f7e6529acc1921913560a4b816c46dce7df4a4d29f9f11a3dc0d4213bb76d043251e + languageName: node + linkType: hard + +"typescript@patch:typescript@npm%3A^5.4.3 <5.5.0#optional!builtin": + version: 5.4.5 + resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=5adc0c" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/db2ad2a16ca829f50427eeb1da155e7a45e598eec7b086d8b4e8ba44e5a235f758e606d681c66992230d3fc3b8995865e5fd0b22a2c95486d0b3200f83072ec9 + languageName: node + linkType: hard + +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 10c0/bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501 + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 10c0/6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10c0/cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + languageName: node + linkType: hard + +"uri-js@npm:^4.2.2": + version: 4.4.1 + resolution: "uri-js@npm:4.4.1" + dependencies: + punycode: "npm:^2.1.0" + checksum: 10c0/4ef57b45aa820d7ac6496e9208559986c665e49447cb072744c13b66925a362d96dd5a46c4530a6b8e203e5db5fe849369444440cb22ecfc26c679359e5dfa3c + languageName: node + linkType: hard + +"v8-compile-cache-lib@npm:^3.0.1": + version: 3.0.1 + resolution: "v8-compile-cache-lib@npm:3.0.1" + checksum: 10c0/bdc36fb8095d3b41df197f5fb6f11e3a26adf4059df3213e3baa93810d8f0cc76f9a74aaefc18b73e91fe7e19154ed6f134eda6fded2e0f1c8d2272ed2d2d391 + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 10c0/449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + languageName: node + linkType: hard + +"word-wrap@npm:^1.2.5": + version: 1.2.5 + resolution: "word-wrap@npm:1.2.5" + checksum: 10c0/e0e4a1ca27599c92a6ca4c32260e8a92e8a44f4ef6ef93f803f8ed823f486e0889fc0b93be4db59c8d51b3064951d25e43d434e95dc8c960cc3a63d65d00ba20 + languageName: node + linkType: hard + +"workerpool@npm:^6.5.1": + version: 6.5.1 + resolution: "workerpool@npm:6.5.1" + checksum: 10c0/58e8e969782292cb3a7bfba823f1179a7615250a0cefb4841d5166234db1880a3d0fe83a31dd8d648329ec92c2d0cd1890ad9ec9e53674bb36ca43e9753cdeac + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 + languageName: node + linkType: hard + +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard + +"yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.9": + version: 20.2.9 + resolution: "yargs-parser@npm:20.2.9" + checksum: 10c0/0685a8e58bbfb57fab6aefe03c6da904a59769bd803a722bb098bd5b0f29d274a1357762c7258fb487512811b8063fb5d2824a3415a0a4540598335b3b086c72 + languageName: node + linkType: hard + +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 + languageName: node + linkType: hard + +"yargs-unparser@npm:^2.0.0": + version: 2.0.0 + resolution: "yargs-unparser@npm:2.0.0" + dependencies: + camelcase: "npm:^6.0.0" + decamelize: "npm:^4.0.0" + flat: "npm:^5.0.2" + is-plain-obj: "npm:^2.1.0" + checksum: 10c0/a5a7d6dc157efa95122e16780c019f40ed91d4af6d2bac066db8194ed0ec5c330abb115daa5a79ff07a9b80b8ea80c925baacf354c4c12edd878c0529927ff03 + languageName: node + linkType: hard + +"yargs@npm:^16.2.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" + dependencies: + cliui: "npm:^7.0.2" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.0" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^20.2.2" + checksum: 10c0/b1dbfefa679848442454b60053a6c95d62f2d2e21dd28def92b647587f415969173c6e99a0f3bab4f1b67ee8283bf735ebe3544013f09491186ba9e8a9a2b651 + languageName: node + linkType: hard + +"yargs@npm:^17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: 10c0/ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 + languageName: node + linkType: hard + +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1" + checksum: 10c0/0732468dd7622ed8a274f640f191f3eaf1f39d5349a1b72836df484998d7d9807fbea094e2f5486d6b0cd2414aad5775972df0e68f8604db89a239f0f4bf7443 + languageName: node + linkType: hard + +"yocto-queue@npm:^0.1.0": + version: 0.1.0 + resolution: "yocto-queue@npm:0.1.0" + checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f + languageName: node + linkType: hard