Enforce HTTPS / Prevent Reverse Tabnabbing (#773)

* working version of test custom rule config

* setting no imports to false so tests will pass

* adding anchor blank noopener rule, rule currently off to allow tests to pass

* removing copied code from tslint-microsoft-contrib

* adding tslint-microsoft-contrib to dev deps

* extending tslint for external http rule

* locking tslint-microsoft-contrib version and turning on target blank noopener rule

* final fixes for pull #663

* add noopener noreferrer as needed

* fixing false positives for a tags without href

* really fix linting errors

* fix imports

* remove accidently(?) added LedgerNano duplicate file
This commit is contained in:
Jack Clancy 2018-01-10 00:17:52 -05:00 committed by Daniel Ternyak
parent 6e2b74c79a
commit 26619e28cc
22 changed files with 266 additions and 31 deletions

View File

@ -93,14 +93,22 @@ export default class AccountInfo extends React.Component<Props, State> {
<ul className="AccountInfo-list"> <ul className="AccountInfo-list">
{!!blockExplorer && ( {!!blockExplorer && (
<li className="AccountInfo-list-item"> <li className="AccountInfo-list-item">
<a href={blockExplorer.address(address)} target="_blank"> <a
href={blockExplorer.address(address)}
target="_blank"
rel="noopener noreferrer"
>
{`${network.name} (${blockExplorer.name})`} {`${network.name} (${blockExplorer.name})`}
</a> </a>
</li> </li>
)} )}
{!!tokenExplorer && ( {!!tokenExplorer && (
<li className="AccountInfo-list-item"> <li className="AccountInfo-list-item">
<a href={tokenExplorer.address(address)} target="_blank"> <a
href={tokenExplorer.address(address)}
target="_blank"
rel="noopener noreferrer"
>
{`Tokens (${tokenExplorer.name})`} {`Tokens (${tokenExplorer.name})`}
</a> </a>
</li> </li>

View File

@ -58,6 +58,7 @@ export default class Promos extends React.Component<{}, State> {
className="Promos-promo" className="Promos-promo"
key={promo.href} key={promo.href}
target="_blank" target="_blank"
rel="noopener noreferrer"
href={promo.href} href={promo.href}
style={{ backgroundColor: promo.color }} style={{ backgroundColor: promo.color }}
> >

View File

@ -19,7 +19,7 @@ const ErrorScreen: React.SFC<Props> = ({ error }) => {
Please contact{' '} Please contact{' '}
<a <a
target="_blank" target="_blank"
rel="noopener" rel="noopener noreferrer"
href={`mailto:support@myetherwallet.com?Subject=${SUBJECT}&body=${DESCRIPTION}`} href={`mailto:support@myetherwallet.com?Subject=${SUBJECT}&body=${DESCRIPTION}`}
> >
support@myetherwallet.com support@myetherwallet.com

View File

@ -14,7 +14,12 @@ const TransactionSucceeded = ({ txHash, blockExplorer }: TransactionSucceededPro
return ( return (
<div> <div>
<p>{translateRaw('SUCCESS_3') + txHash}</p> <p>{translateRaw('SUCCESS_3') + txHash}</p>
<a className="btn btn-xs btn-info string" href={txHashLink} target="_blank" rel="noopener"> <a
className="btn btn-xs btn-info string"
href={txHashLink}
target="_blank"
rel="noopener noreferrer"
>
Verify Transaction Verify Transaction
</a> </a>
</div> </div>

View File

@ -61,7 +61,11 @@ export default class GasPriceDropdown extends Component<Props> {
<code>21 GWEI</code>. <code>21 GWEI</code>.
</p> </p>
<p> <p>
<a href={`${knowledgeBaseURL}/gas/what-is-gas-ethereum`} target="_blank"> <a
href={`${knowledgeBaseURL}/gas/what-is-gas-ethereum`}
target="_blank"
rel="noopener noreferrer"
>
Read more Read more
</a> </a>
</p> </p>

View File

@ -35,7 +35,13 @@ class NavigationLink extends React.Component<Props, {}> {
const linkEl = const linkEl =
link.external || !link.to ? ( link.external || !link.to ? (
<a className={linkClasses} href={link.to} aria-label={linkLabel} target="_blank"> <a
className={linkClasses}
href={link.to}
aria-label={linkLabel}
target="_blank"
rel="noopener noreferrer"
>
{translate(link.name)} {translate(link.name)}
</a> </a>
) : ( ) : (

View File

@ -292,7 +292,11 @@ class DeterministicWalletsModalClass extends React.Component<Props, State> {
)} )}
</td> </td>
<td> <td>
<a target="_blank" href={`https://ethplorer.io/address/${wallet.address}`}> <a
target="_blank"
href={`https://ethplorer.io/address/${wallet.address}`}
rel="noopener noreferrer"
>
<i className="DWModal-addresses-table-more" /> <i className="DWModal-addresses-table-more" />
</a> </a>
</td> </td>
@ -310,7 +314,9 @@ function mapStateToProps(state: AppState) {
}; };
} }
export const DeterministicWalletsModal = connect(mapStateToProps, { const DeterministicWalletsModal = connect(mapStateToProps, {
getDeterministicWallets, getDeterministicWallets,
setDesiredToken setDesiredToken
})(DeterministicWalletsModalClass); })(DeterministicWalletsModalClass);
export default DeterministicWalletsModal;

View File

@ -1,7 +1,7 @@
import './LedgerNano.scss'; import './LedgerNano.scss';
import React, { Component } from 'react'; import React, { Component } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import { DeterministicWalletsModal } from './DeterministicWalletsModal'; import DeterministicWalletsModal from './DeterministicWalletsModal';
import { LedgerWallet } from 'libs/wallet'; import { LedgerWallet } from 'libs/wallet';
import Ledger3 from 'vendor/ledger3'; import Ledger3 from 'vendor/ledger3';
import LedgerEth from 'vendor/ledger-eth'; import LedgerEth from 'vendor/ledger-eth';
@ -81,7 +81,7 @@ export class LedgerNanoSDecrypt extends Component<Props, State> {
className="LedgerDecrypt-buy btn btn-sm btn-default" className="LedgerDecrypt-buy btn btn-sm btn-default"
href="https://www.ledgerwallet.com/r/fa4b?path=/products/" href="https://www.ledgerwallet.com/r/fa4b?path=/products/"
target="_blank" target="_blank"
rel="noopener" rel="noopener noreferrer"
> >
{translate('Dont have a Ledger? Order one now!')} {translate('Dont have a Ledger? Order one now!')}
</a> </a>
@ -92,9 +92,9 @@ export class LedgerNanoSDecrypt extends Component<Props, State> {
Guides: Guides:
<div> <div>
<a <a
href="http://support.ledgerwallet.com/knowledge_base/topics/how-to-use-myetherwallet-with-ledger" href="https://support.ledgerwallet.com/knowledge_base/topics/how-to-use-myetherwallet-with-ledger"
target="_blank" target="_blank"
rel="noopener" rel="noopener noreferrer"
> >
How to use MyEtherWallet with your Nano S How to use MyEtherWallet with your Nano S
</a> </a>
@ -103,7 +103,7 @@ export class LedgerNanoSDecrypt extends Component<Props, State> {
<a <a
href="https://ledger.groovehq.com/knowledge_base/topics/how-to-secure-your-eth-tokens-augur-rep-dot-dot-dot-with-your-nano-s" href="https://ledger.groovehq.com/knowledge_base/topics/how-to-secure-your-eth-tokens-augur-rep-dot-dot-dot-with-your-nano-s"
target="_blank" target="_blank"
rel="noopener" rel="noopener noreferrer"
> >
How to secure your tokens with your Nano S How to secure your tokens with your Nano S
</a> </a>

View File

@ -2,7 +2,7 @@ import { mnemonicToSeed, validateMnemonic } from 'bip39';
import DPATHS from 'config/dpaths'; import DPATHS from 'config/dpaths';
import React, { Component } from 'react'; import React, { Component } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import { DeterministicWalletsModal } from './DeterministicWalletsModal'; import DeterministicWalletsModal from './DeterministicWalletsModal';
import { formatMnemonic } from 'utils/formatters'; import { formatMnemonic } from 'utils/formatters';
const DEFAULT_PATH = DPATHS.MNEMONIC[0].value; const DEFAULT_PATH = DPATHS.MNEMONIC[0].value;

View File

@ -3,7 +3,7 @@ import { TrezorWallet } from 'libs/wallet';
import React, { Component } from 'react'; import React, { Component } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import TrezorConnect from 'vendor/trezor-connect'; import TrezorConnect from 'vendor/trezor-connect';
import { DeterministicWalletsModal } from './DeterministicWalletsModal'; import DeterministicWalletsModal from './DeterministicWalletsModal';
import './Trezor.scss'; import './Trezor.scss';
import { Spinner } from 'components/ui'; import { Spinner } from 'components/ui';
const DEFAULT_PATH = DPATHS.TREZOR[0].value; const DEFAULT_PATH = DPATHS.TREZOR[0].value;
@ -53,7 +53,7 @@ export class TrezorDecrypt extends Component<Props, State> {
className="TrezorDecrypt-buy btn btn-sm btn-default" className="TrezorDecrypt-buy btn btn-sm btn-default"
href="https://trezor.io/?a=myetherwallet.com" href="https://trezor.io/?a=myetherwallet.com"
target="_blank" target="_blank"
rel="noopener" rel="noopener noreferrer"
> >
{translate('Dont have a TREZOR? Order one now!')} {translate('Dont have a TREZOR? Order one now!')}
</a> </a>
@ -65,7 +65,7 @@ export class TrezorDecrypt extends Component<Props, State> {
<a <a
href="https://blog.trezor.io/trezor-integration-with-myetherwallet-3e217a652e08" href="https://blog.trezor.io/trezor-integration-with-myetherwallet-3e217a652e08"
target="_blank" target="_blank"
rel="noopener" rel="noopener noreferrer"
> >
How to use TREZOR with MyEtherWallet How to use TREZOR with MyEtherWallet
</a> </a>

View File

@ -11,7 +11,7 @@ interface Props {
const Help = ({ size = 'x1', link }: Props) => { const Help = ({ size = 'x1', link }: Props) => {
return ( return (
<a href={link} className={`Help Help-${size}`} target={'_blank'}> <a href={link} className={`Help Help-${size}`} target="_blank" rel="noopener noreferrer">
<img src={icon} /> <img src={icon} />
</a> </a>
); );

View File

@ -36,7 +36,7 @@ interface NewTabLinkProps extends AAttributes {
} }
const NewTabLink = ({ content, children, ...rest }: NewTabLinkProps) => ( const NewTabLink = ({ content, children, ...rest }: NewTabLinkProps) => (
<a target="_blank" rel="noopener" {...rest}> <a target="_blank" rel="noopener noreferrer" {...rest}>
{content || children} {content || children}
</a> </a>
); );

View File

@ -17,6 +17,7 @@ const Help = () => (
<a <a
href="https://www.reddit.com/r/ethereum/comments/47nkoi/psa_check_your_ethaddressorg_wallets_and_any/d0eo45o" href="https://www.reddit.com/r/ethereum/comments/47nkoi/psa_check_your_ethaddressorg_wallets_and_any/d0eo45o"
target="_blank" target="_blank"
rel="noopener noreferrer"
> >
<span className="text-danger">{translate('HELP_Warning')}</span> <span className="text-danger">{translate('HELP_Warning')}</span>
</a> </a>
@ -25,7 +26,7 @@ const Help = () => (
<li> <li>
<h3> <h3>
This page is deprecated. Please check out our more up-to-date and searchable{' '} This page is deprecated. Please check out our more up-to-date and searchable{' '}
<a href={knowledgeBaseURL} target="_blank"> <a href={knowledgeBaseURL} target="_blank" rel="noopener noreferrer">
Knowledge Base.{' '} Knowledge Base.{' '}
</a> </a>
</h3> </h3>

View File

@ -21,7 +21,7 @@ export default class BitcoinQR extends Component<Props, {}> {
Orders that take too long will have to be processed manually &amp; and may delay the Orders that take too long will have to be processed manually &amp; and may delay the
amount of time it takes to receive your coins. amount of time it takes to receive your coins.
<br /> <br />
<a href="https://shapeshift.io/#/btcfee" target="_blank" rel="noopener"> <a href="https://shapeshift.io/#/btcfee" target="_blank" rel="noopener noreferrer">
Please use the recommended TX fees seen here. Please use the recommended TX fees seen here.
</a> </a>
</p> </p>

View File

@ -88,7 +88,12 @@ export default class CurrentRates extends Component<Props> {
<section className="SwapRates-panel row"> <section className="SwapRates-panel row">
{children} {children}
<a className="SwapRates-panel-logo" href={providerURL} target="_blank"> <a
className="SwapRates-panel-logo"
href={providerURL}
target="_blank"
rel="noopener noreferrer"
>
<img src={providerLogo} width={120} height={49} /> <img src={providerLogo} width={120} height={49} />
</a> </a>
</section> </section>

View File

@ -27,7 +27,12 @@ export default class SwapInfoHeaderTitle extends Component<SwapInfoHeaderTitlePr
<h3 className="SwapInfo-top-title">{translate('SWAP_information')}</h3> <h3 className="SwapInfo-top-title">{translate('SWAP_information')}</h3>
</div> </div>
<div className="col-xs-3"> <div className="col-xs-3">
<a className="SwapInfo-top-logo" href={bityReferralURL} target="_blank" rel="noopener"> <a
className="SwapInfo-top-logo"
href={bityReferralURL}
target="_blank"
rel="noopener noreferrer"
>
<img className="SwapInfo-top-logo-img" src={logoToRender} /> <img className="SwapInfo-top-logo-img" src={logoToRender} />
</a> </a>
</div> </div>

View File

@ -49,7 +49,7 @@ export default class SwapProgress extends Component<Props, State> {
if (destinationId !== 'BTC') { if (destinationId !== 'BTC') {
link = bityConfig.ETHTxExplorer(outputTx); link = bityConfig.ETHTxExplorer(outputTx);
linkElement = ( linkElement = (
<a href={link} target="_blank" rel="noopener"> <a href={link} target="_blank" rel="noopener noreferrer">
{notificationMessage} {notificationMessage}
</a> </a>
); );
@ -57,7 +57,7 @@ export default class SwapProgress extends Component<Props, State> {
} else { } else {
link = bityConfig.BTCTxExplorer(outputTx); link = bityConfig.BTCTxExplorer(outputTx);
linkElement = ( linkElement = (
<a href={link} target="_blank" rel="noopener"> <a href={link} target="_blank" rel="noopener noreferrer">
{notificationMessage} {notificationMessage}
</a> </a>
); );

View File

@ -22,7 +22,7 @@
</p> </p>
<p> <p>
If you are not sure why you are seeing this message, or are unsure of how to enable Javascript, please visit If you are not sure why you are seeing this message, or are unsure of how to enable Javascript, please visit
<a href="https://www.enable-javascript.com/" rel="noopener" target="_blank">enable-javascript.com</a> <a href="https://www.enable-javascript.com/" rel="noopener noreferrer" target="_blank">enable-javascript.com</a>
to learn more. to learn more.
</p> </p>
</div> </div>
@ -41,18 +41,18 @@
to a laptop or computer to continue using MyEtherWallet. to a laptop or computer to continue using MyEtherWallet.
</p> </p>
<div class="BadBrowser-content-browsers is-desktop"> <div class="BadBrowser-content-browsers is-desktop">
<a class="BadBrowser-content-browsers-browser firefox" href="https://www.mozilla.org/en-US/firefox/new/" rel="noopener" target="_blank"> <a class="BadBrowser-content-browsers-browser firefox" href="https://www.mozilla.org/en-US/firefox/new/" rel="noopener noreferrer" target="_blank">
<span class="BadBrowser-content-browsers-browser-name"> <span class="BadBrowser-content-browsers-browser-name">
Firefox Firefox
</span> </span>
</a> </a>
<a class="BadBrowser-content-browsers-browser chrome" href="https://www.google.com/chrome/browser/desktop/index.html" rel="noopener" <a class="BadBrowser-content-browsers-browser chrome" href="https://www.google.com/chrome/browser/desktop/index.html" rel="noopener noreferrer"
target="_blank"> target="_blank">
<span class="BadBrowser-content-browsers-browser-name"> <span class="BadBrowser-content-browsers-browser-name">
Chrome Chrome
</span> </span>
</a> </a>
<a class="BadBrowser-content-browsers-browser opera" href="http://www.opera.com/" rel="noopener" target="_blank"> <a class="BadBrowser-content-browsers-browser opera" href="http://www.opera.com/" rel="noopener noreferrer" target="_blank">
<span class="BadBrowser-content-browsers-browser-name"> <span class="BadBrowser-content-browsers-browser-name">
Opera Opera
</span> </span>

View File

@ -0,0 +1,95 @@
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
exports.__esModule = true;
var ts = require("typescript");
var Lint = require("tslint");
var ErrorTolerantWalker_1 = require("../node_modules/tslint-microsoft-contrib/utils/ErrorTolerantWalker");
var JsxAttribute_1 = require("../node_modules/tslint-microsoft-contrib/utils/JsxAttribute");
var FAILURE_STRING = 'Anchor tags with an external link must use https';
/**
* Implementation of the no-external-http-link rule.
*/
var Rule = /** @class */ (function (_super) {
__extends(Rule, _super);
function Rule() {
return _super !== null && _super.apply(this, arguments) || this;
}
Rule.prototype.apply = function (sourceFile) {
if (sourceFile.languageVariant === ts.LanguageVariant.JSX) {
return this.applyWithWalker(new NoExternalHttpLinkRuleWalker(sourceFile, this.getOptions()));
}
else {
return [];
}
};
Rule.metadata = {
ruleName: 'tno-external-http-link',
type: 'functionality',
description: 'Anchor tags with an external link must use https',
options: null,
optionsDescription: '',
typescriptOnly: true,
issueClass: 'SDL',
issueType: 'Error',
severity: 'Critical',
level: 'Mandatory',
group: 'Security',
commonWeaknessEnumeration: '242,676'
};
return Rule;
}(Lint.Rules.AbstractRule));
exports.Rule = Rule;
var NoExternalHttpLinkRuleWalker = /** @class */ (function (_super) {
__extends(NoExternalHttpLinkRuleWalker, _super);
function NoExternalHttpLinkRuleWalker() {
return _super !== null && _super.apply(this, arguments) || this;
}
NoExternalHttpLinkRuleWalker.prototype.visitJsxElement = function (node) {
var openingElement = node.openingElement;
this.validateOpeningElement(openingElement);
_super.prototype.visitJsxElement.call(this, node);
};
NoExternalHttpLinkRuleWalker.prototype.visitJsxSelfClosingElement = function (node) {
this.validateOpeningElement(node);
_super.prototype.visitJsxSelfClosingElement.call(this, node);
};
NoExternalHttpLinkRuleWalker.prototype.validateOpeningElement = function (openingElement) {
if (openingElement.tagName.getText() === 'a') {
var allAttributes = JsxAttribute_1.getJsxAttributesFromJsxElement(openingElement);
var href = allAttributes.href;
if (href !== null && !isSafeHrefAttributeValue(href) && JsxAttribute_1.getStringLiteral(href) !== 'undefined') {
this.addFailureAt(openingElement.getStart(), openingElement.getWidth(), FAILURE_STRING);
}
}
};
return NoExternalHttpLinkRuleWalker;
}(ErrorTolerantWalker_1.ErrorTolerantWalker));
function isSafeHrefAttributeValue(attribute) {
if (JsxAttribute_1.isEmpty(attribute)) {
return false;
}
if (attribute.initializer.kind === ts.SyntaxKind.JsxExpression) {
var expression = attribute.initializer;
if (expression.expression !== null &&
expression.expression.kind !== ts.SyntaxKind.StringLiteral) {
return true; // attribute value is not a string literal, so do not validate
}
}
var stringValue = JsxAttribute_1.getStringLiteral(attribute);
if (stringValue === '#') {
return true;
}
else if (stringValue === null || stringValue.length === 0) {
return false;
}
return stringValue.indexOf('https://') >= 0;
}

View File

@ -0,0 +1,96 @@
import * as ts from 'typescript';
import * as Lint from 'tslint';
import { ErrorTolerantWalker } from '../node_modules/tslint-microsoft-contrib/utils/ErrorTolerantWalker';
import { ExtendedMetadata } from '../node_modules/tslint-microsoft-contrib/utils/ExtendedMetadata';
import { Utils } from '../node_modules/tslint-microsoft-contrib/utils/Utils';
import {
getJsxAttributesFromJsxElement,
getStringLiteral,
isEmpty
} from '../node_modules/tslint-microsoft-contrib/utils/JsxAttribute';
const FAILURE_STRING = 'Anchor tags with an external link must use https';
/**
* Implementation of the no-external-http-link rule.
*/
export class Rule extends Lint.Rules.AbstractRule {
public static metadata: ExtendedMetadata = {
ruleName: 'tno-external-http-link',
type: 'functionality',
description: 'Anchor tags with an external link must use https',
options: null,
optionsDescription: '',
typescriptOnly: true,
issueClass: 'SDL',
issueType: 'Error',
severity: 'Critical',
level: 'Mandatory',
group: 'Security',
commonWeaknessEnumeration: '242,676'
};
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
if (sourceFile.languageVariant === ts.LanguageVariant.JSX) {
return this.applyWithWalker(new NoExternalHttpLinkRuleWalker(sourceFile, this.getOptions()));
} else {
return [];
}
}
}
class NoExternalHttpLinkRuleWalker extends ErrorTolerantWalker {
protected visitJsxElement(node: ts.JsxElement): void {
const openingElement: ts.JsxOpeningElement = node.openingElement;
this.validateOpeningElement(openingElement);
super.visitJsxElement(node);
}
protected visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): void {
this.validateOpeningElement(node);
super.visitJsxSelfClosingElement(node);
}
private validateOpeningElement(openingElement: ts.JsxOpeningLikeElement): void {
if (openingElement.tagName.getText() === 'a') {
const allAttributes: { [propName: string]: ts.JsxAttribute } = getJsxAttributesFromJsxElement(
openingElement
);
const href: ts.JsxAttribute = allAttributes.href;
if (
href !== null &&
!isSafeHrefAttributeValue(href) &&
getStringLiteral(href) !== 'undefined'
) {
this.addFailureAt(openingElement.getStart(), openingElement.getWidth(), FAILURE_STRING);
}
}
}
}
function isSafeHrefAttributeValue(attribute: ts.JsxAttribute): boolean {
if (isEmpty(attribute)) {
return false;
}
if (attribute.initializer.kind === ts.SyntaxKind.JsxExpression) {
const expression: ts.JsxExpression = <ts.JsxExpression>attribute.initializer;
if (
expression.expression !== null &&
expression.expression.kind !== ts.SyntaxKind.StringLiteral
) {
return true; // attribute value is not a string literal, so do not validate
}
}
const stringValue = getStringLiteral(attribute);
if (stringValue === '#') {
return true;
} else if (stringValue === null || stringValue.length === 0) {
return false;
}
return stringValue.indexOf('https://') >= 0;
}

View File

@ -109,6 +109,7 @@
"ts-loader": "3.2.0", "ts-loader": "3.2.0",
"tslint": "5.8.0", "tslint": "5.8.0",
"tslint-config-prettier": "1.6.0", "tslint-config-prettier": "1.6.0",
"tslint-microsoft-contrib": "5.0.1",
"tslint-react": "3.3.3", "tslint-react": "3.3.3",
"types-rlp": "0.0.1", "types-rlp": "0.0.1",
"typescript": "2.6.2", "typescript": "2.6.2",

View File

@ -23,7 +23,9 @@
"no-var-requires": false, "no-var-requires": false,
"jsx-wrap-multiline": false, "jsx-wrap-multiline": false,
"comment-format": false, "comment-format": false,
"ordered-imports": false "ordered-imports": false,
"react-anchor-blank-noopener": true,
"no-external-http-link": true
}, },
"rulesDirectory": [] "rulesDirectory": ["node_modules/tslint-microsoft-contrib", "custom_linting_rules"]
} }