├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── index.js ├── no-pattern-match.js ├── regexes.js └── utils.js ├── index.d.ts ├── package-lock.json ├── package.json ├── src ├── index.ts ├── no-pattern-match.ts ├── regexes.ts └── utils.ts ├── staging ├── has-no-secret.js ├── has-no-secret.json ├── has-secret.js ├── has-secret.json ├── json-flat.eslintrc.js ├── jsonc-flat.eslintrc.js ├── jsonc.eslintrc.js ├── mixed-flat.eslintrc.js ├── mixed.eslintrc.js ├── normal-flat.eslintrc.js ├── normal.eslintrc.js └── staging.spec.js ├── tests └── lib │ └── rules │ ├── index.ts │ ├── no-pattern-match.ts │ ├── no-secrets.ts │ └── rule-testers.ts ├── tsconfig.json └── v1tov2.md /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | # Have to use 18.x and up because later versions of eslint require structuredClone 15 | node-version: [18.x, 20.x, 22.x] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Run tests against node ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: "npm" 23 | - run: npm ci 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | *.log 3 | .vs/ 4 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests/* 2 | /node_modules/ 3 | *.log 4 | staging/* 5 | .travis.yml 6 | .github/* 7 | src/* 8 | tsconfig.json 9 | v1tov2.md 10 | CHANGELOG.md 11 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 2.2.1 4 | 5 | ### Minor 6 | 7 | - Add support for the official ESLint json parser 8 | 9 | ## 2.1.0 10 | 11 | ### Major 12 | 13 | - Removed support for non-supported node & npm versions 14 | - Removed support for ESLint 4 and below 15 | 16 | ### Minor 17 | 18 | - Module is now written in typescript 19 | 20 | ## 1.1.2 21 | 22 | ### Minor 23 | 24 | - Added typings (thank you @mjlehrke) 25 | 26 | ### Patch 27 | 28 | - Patched some packages 29 | 30 | ## 1.0.2 31 | 32 | ### Major 33 | 34 | - This package has been out long enough for 1.0.0 release 35 | 36 | ### Minor 37 | 38 | - Added support and tests for ESLint "flat config" 39 | 40 | ### Patch 41 | 42 | - Several packages have been updated and patched 43 | 44 | ## 0.9.1 45 | 46 | ### Patch 47 | 48 | - Pre-release before adding support for eslint flag config and to fix versioning 49 | 50 | ## 0.8.9 51 | 52 | ### Minor 53 | 54 | - Replaced how JSON document scanning worked so it works with other plugins 55 | 56 | ## 0.7.9 57 | 58 | ### Minor 59 | 60 | - Add support for linting comments 61 | 62 | ## 0.6.9 63 | 64 | ### Patch 65 | 66 | - Add eslint 7 unit testing 67 | 68 | ## 0.6.8 69 | 70 | ### Patch 71 | 72 | - Security updates 73 | - Removed eslint 5 testing 74 | 75 | ## 0.6.5 76 | 77 | ### Patch 78 | 79 | - Security updates 80 | 81 | ## 0.6.4 82 | 83 | ### Minor 84 | 85 | - Added support for scanning JSON documents 86 | 87 | ## 0.5.4 88 | 89 | ### Minor 90 | 91 | - Added support for two new options 92 | - `additionalDelimiters`: In addition to splitting the string by whitespace, tokens will be further split by these delimiters 93 | - `ignoreCase`: Ignores character case when calculating entropy. This could lead to some false negatives 94 | 95 | ## 0.3.4 96 | 97 | ### Patch 98 | 99 | - Security updates 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nick Deis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/nickdeis/eslint-plugin-no-secrets/actions/workflows/main.yml/badge.svg)](https://github.com/nickdeis/eslint-plugin-no-secrets/actions/workflows/main.yml/badge.svg) 2 | 3 | # eslint-plugin-no-secrets 4 | 5 | An eslint rule that searches for potential secrets/keys in code and JSON files. 6 | 7 | This plugin has two rules: 8 | 9 | - `no-secrets`: Find potential secrets using cryptographic entropy or patterns in the AST (acts like a standard eslint rule, more configurable) 10 | - `no-pattern-match`: Find potential secrets in text (acts like `grep`, less configurable, but potentially more flexible) 11 | 12 | --- 13 | 14 | 15 | 16 | - 1. [Usage](#Usage) 17 | - 1.1. [Flat config](#Flatconfig) 18 | - 1.2. [eslintrc](#eslintrc) 19 | - 1.3. [Include JSON files](#IncludeJSONfiles) 20 | - 1.3.1. [Include JSON files with in "flat configs"](#IncludeJSONfileswithinflatconfigs) 21 | - 2. [`no-secrets`](#no-secrets) 22 | - 2.1. [`no-secrets` examples](#no-secretsexamples) 23 | - 2.2. [When it's really not a secret](#Whenitsreallynotasecret) 24 | - 2.2.1. [ Either disable it with a comment](#Eitherdisableitwithacomment) 25 | - 2.2.2. [ use the `ignoreContent` to ignore certain content](#usetheignoreContenttoignorecertaincontent) 26 | - 2.2.3. [ Use `ignoreIdentifiers` to ignore certain variable/property names](#UseignoreIdentifierstoignorecertainvariablepropertynames) 27 | - 2.2.4. [ Use `additionalDelimiters` to further split up tokens](#UseadditionalDelimiterstofurthersplituptokens) 28 | - 2.3. [`no-secrets` Options](#no-secretsOptions) 29 | - 3. [`no-pattern-match`](#no-pattern-match) 30 | - 3.1. [`no-pattern-match` options](#no-pattern-matchoptions) 31 | - 4. [Acknowledgements](#Acknowledgements) 32 | 33 | 37 | 38 | 39 | ## 1. Usage 40 | 41 | `npm i -D eslint-plugin-no-secrets` 42 | 43 | ### 1.1. Flat config 44 | 45 | _eslint.config.js_ 46 | 47 | ```js 48 | import noSecrets from "eslint-plugin-no-secrets"; 49 | 50 | export default [ 51 | { 52 | files: ["**/*.js"], 53 | plugins: { 54 | "no-secrets": noSecrets, 55 | }, 56 | rules: { 57 | "no-secrets/no-secrets": "error", 58 | }, 59 | }, 60 | ]; 61 | ``` 62 | 63 | ### 1.2. eslintrc 64 | 65 | _.eslintrc_ 66 | 67 | ```json 68 | { 69 | "plugins": ["no-secrets"], 70 | "rules": { 71 | "no-secrets/no-secrets": "error" 72 | } 73 | } 74 | ``` 75 | 76 | ```js 77 | //Found a string with entropy 4.3 : "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva" 78 | const A_SECRET = 79 | "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; 80 | //Found a string that matches "AWS API Key" : "AKIAIUWUUQQN3GNUA88V" 81 | const AWS_TOKEN = "AKIAIUWUUQQN3GNUA88V"; 82 | ``` 83 | 84 | ### 1.3. Include JSON files 85 | 86 | To include JSON files, install `eslint-plugin-jsonc` or `@eslint/json` (if using ESLint version 9.6 or above) 87 | 88 | `npm install --save-dev eslint-plugin-jsonc` 89 | 90 | Then in your `.eslint` configuration file, extend the jsonc base config 91 | 92 | ```json 93 | { 94 | "extends": ["plugin:jsonc/base"] 95 | } 96 | ``` 97 | 98 | or if you are using ESLint 9.6 or above 99 | 100 | ```typescript 101 | module.exports = [ 102 | { 103 | plugins: { 104 | json, 105 | "no-secrets": noSecret, 106 | }, 107 | }, 108 | { 109 | files: ["**/*.json"], 110 | language: "json/json", 111 | .... 112 | }, 113 | ]; 114 | ``` 115 | 116 | #### 1.3.1. Include JSON files with in "flat configs" 117 | 118 | _eslint.config.js_ 119 | 120 | ```js 121 | import noSecrets from "eslint-plugin-no-secrets"; 122 | import jsoncExtend from "eslint-plugin-jsonc"; 123 | 124 | export default [ 125 | ...jsoncExtend.configs["flat/recommended-with-jsonc"], 126 | { 127 | languageOptions: { ecmaVersion: 6 }, 128 | plugins: { 129 | "no-secrets": noSecrets, 130 | }, 131 | rules: { 132 | "no-secrets/no-secrets": "error", 133 | }, 134 | }, 135 | ]; 136 | ``` 137 | 138 | ## 2. `no-secrets` 139 | 140 | `no-secrets` is a rule that does two things: 141 | 142 | 1. Search for patterns that often contain sensitive information 143 | 2. Measure cryptographic entropy to find potentially leaked secrets/passwords 144 | 145 | It's modeled after early [truffleHog](https://github.com/dxa4481/truffleHog), but acts on ECMAscripts AST. This allows closer inspection into areas where secrets are commonly leaked like string templates or comments. 146 | 147 | ### 2.1. `no-secrets` examples 148 | 149 | Decrease the tolerance for entropy 150 | 151 | ```json 152 | { 153 | "plugins": ["no-secrets"], 154 | "rules": { 155 | "no-secrets/no-secrets": ["error", { "tolerance": 3.2 }] 156 | } 157 | } 158 | ``` 159 | 160 | Add additional patterns to check for certain token formats. 161 | Standard patterns can be found [here](./regexes.js) 162 | 163 | ```json 164 | { 165 | "plugins": ["no-secrets"], 166 | "rules": { 167 | "no-secrets/no-secrets": [ 168 | "error", 169 | { 170 | "additionalRegexes": { 171 | "Basic Auth": "Authorization: Basic [A-Za-z0-9+/=]*" 172 | } 173 | } 174 | ] 175 | } 176 | } 177 | ``` 178 | 179 | ### 2.2. When it's really not a secret 180 | 181 | #### 2.2.1. Either disable it with a comment 182 | 183 | ```javascript 184 | // Set of potential base64 characters 185 | // eslint-disable-next-line no-secrets/no-secrets 186 | const BASE64_CHARS = 187 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 188 | ``` 189 | 190 | This will tell future maintainers of the codebase that this suspicious string isn't an oversight 191 | 192 | #### 2.2.2. use the `ignoreContent` to ignore certain content 193 | 194 | ```json 195 | { 196 | "plugins": ["no-secrets"], 197 | "rules": { 198 | "no-secrets/no-secrets": ["error", { "ignoreContent": "^ABCD" }] 199 | } 200 | } 201 | ``` 202 | 203 | #### 2.2.3. Use `ignoreIdentifiers` to ignore certain variable/property names 204 | 205 | ```json 206 | { 207 | "plugins": ["no-secrets"], 208 | "rules": { 209 | "no-secrets/no-secrets": [ 210 | "error", 211 | { "ignoreIdentifiers": ["BASE64_CHARS"] } 212 | ] 213 | } 214 | } 215 | ``` 216 | 217 | #### 2.2.4. Use `additionalDelimiters` to further split up tokens 218 | 219 | Tokens will always be split up by whitespace within a string. However, sometimes words that are delimited by something else (e.g. dashes, periods, camelcase words). You can use `additionalDelimiters` to handle these cases. 220 | 221 | For example, if you want to split words up by the character `.` and by camelcase, you could use this configuration: 222 | 223 | ```json 224 | { 225 | "plugins": ["no-secrets"], 226 | "rules": { 227 | "no-secrets/no-secrets": [ 228 | "error", 229 | { "additionalDelimiters": [".", "(?=[A-Z][a-z])"] } 230 | ] 231 | } 232 | } 233 | ``` 234 | 235 | ### 2.3. `no-secrets` Options 236 | 237 | | Option | Description | Default | Type | 238 | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------- | 239 | | tolerance | Minimum "randomness"/entropy allowed. Only strings **above** this threshold will be shown. | `4` | `number` | 240 | | additionalRegexes | Object of additional patterns to check. Key is check name and value is corresponding pattern | `{}` | {\[regexCheckName:string]:string \| RegExp} | 241 | | ignoreContent | Will ignore the _entire_ string if matched. Expects either a pattern or an array of patterns. This option takes precedent over `additionalRegexes` and the default regular expressions | `[]` | string \| RegExp \| (string\|RegExp)[] | 242 | | ignoreModules | Ignores strings that are an argument in `import()` and `require()` or is the path in an `import` statement. | `true` | `boolean` | 243 | | ignoreIdentifiers | Ignores the values of properties and variables that match a pattern or an array of patterns. | `[]` | string \| RegExp \| (string\|RegExp)[] | 244 | | ignoreCase | Ignores character case when calculating entropy. This could lead to some false negatives | `false` | `boolean` | 245 | | additionalDelimiters | In addition to splitting the string by whitespace, tokens will be further split by these delimiters | `[]` | (string\|RegExp)[] | 246 | 247 | ## 3. `no-pattern-match` 248 | 249 | While this rule was originally made to take advantage of ESLint's AST, sometimes you may want to see if a pattern matches any text in a file, kinda like `grep`. 250 | 251 | For example, if we configure as follows: 252 | 253 | ```js 254 | import noSecrets from "eslint-plugin-no-secrets"; 255 | 256 | //Flat config 257 | 258 | export default [ 259 | { 260 | files: ["**/*.js"], 261 | plugins: { 262 | "no-secrets": noSecret, 263 | }, 264 | rules: { 265 | "no-secrets/no-pattern-match": [ 266 | "error", 267 | { patterns: { SecretJS: /const SECRET/, SecretJSON: /\"SECRET\"/ } }, 268 | ], 269 | }, 270 | }, 271 | ]; 272 | ``` 273 | 274 | We would match `const SECRET`, but not `var SECRET`. We would match keys that were called `"SECRET"` in JSON files if they were configured to be scanned. 275 | 276 | ### 3.1. `no-pattern-match` options 277 | 278 | | Option | Description | Default | Type | 279 | | -------- | ----------------------------------------------------------------- | ------- | ------------------------------------------- | 280 | | patterns | An object of patterns to check the text contents of files against | `{}` | {\[regexCheckName:string]:string \| RegExp} | 281 | 282 | ## 4. Acknowledgements 283 | 284 | Huge thanks to [truffleHog](https://github.com/dxa4481/truffleHog) for the inspiration, the regexes, and the measure of entropy. 285 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.rules = exports.meta = void 0; 7 | const utils_1 = require("./utils"); 8 | const regexes_1 = __importDefault(require("./regexes")); 9 | const no_pattern_match_1 = __importDefault(require("./no-pattern-match")); 10 | function isNonEmptyString(value) { 11 | return !!(value && typeof value === "string"); 12 | } 13 | function checkRegexes(value, patterns) { 14 | return Object.keys(patterns) 15 | .map((name) => { 16 | const pattern = patterns[name]; 17 | const m = value.match(pattern); 18 | if (!m || !m[0]) 19 | return m; 20 | return { name, match: m[0] }; 21 | }) 22 | .filter((payload) => !!payload); 23 | } 24 | function shouldIgnore(value, toIgnore) { 25 | for (let i = 0; i < toIgnore.length; i++) { 26 | if (value.match(toIgnore[i])) 27 | return true; 28 | } 29 | return false; 30 | } 31 | const meta = { 32 | name: "eslint-plugin-no-secrets", 33 | version: "2.1.1", 34 | }; 35 | exports.meta = meta; 36 | const noSecrets = { 37 | meta: { 38 | schema: false, 39 | messages: { 40 | [utils_1.HIGH_ENTROPY]: `Found a string with entropy {{ entropy }} : "{{ token }}"`, 41 | [utils_1.PATTERN_MATCH]: `Found a string that matches "{{ name }}" : "{{ match }}"`, 42 | }, 43 | docs: { 44 | description: "An eslint rule that looks for possible leftover secrets in code", 45 | category: "Best Practices", 46 | }, 47 | }, 48 | create(context) { 49 | var _a; 50 | const { tolerance, additionalRegexes, ignoreContent, ignoreModules, ignoreIdentifiers, additionalDelimiters, ignoreCase, } = (0, utils_1.checkOptions)(context.options[0] || {}); 51 | const sourceCode = context.getSourceCode() || context.sourceCode; 52 | const allPatterns = Object.assign({}, regexes_1.default, additionalRegexes); 53 | const allDelimiters = additionalDelimiters.concat([" "]); 54 | function splitIntoTokens(value) { 55 | let tokens = [value]; 56 | allDelimiters.forEach((delimiter) => { 57 | //@ts-ignore 58 | tokens = tokens.map((token) => token.split(delimiter)); 59 | //flatten 60 | tokens = [].concat.apply([], tokens); 61 | }); 62 | return tokens; 63 | } 64 | function checkEntropy(value) { 65 | value = ignoreCase ? value.toLowerCase() : value; 66 | const tokens = splitIntoTokens(value); 67 | return tokens 68 | .map((token) => { 69 | const entropy = (0, utils_1.shannonEntropy)(token); 70 | return { token, entropy }; 71 | }) 72 | .filter((payload) => tolerance <= payload.entropy); 73 | } 74 | function entropyReport(data, node) { 75 | //Easier to read numbers 76 | data.entropy = Math.round(data.entropy * 100) / 100; 77 | context.report({ 78 | node, 79 | data, 80 | messageId: utils_1.HIGH_ENTROPY, 81 | }); 82 | } 83 | function patternReport(data, node) { 84 | context.report({ 85 | node, 86 | data, 87 | messageId: utils_1.PATTERN_MATCH, 88 | }); 89 | } 90 | function checkString(value, node) { 91 | const idName = (0, utils_1.getIdentifierName)(node); 92 | if (idName && shouldIgnore(idName, ignoreIdentifiers)) 93 | return; 94 | if (!isNonEmptyString(value)) 95 | return; 96 | if (ignoreModules && (0, utils_1.isModulePathString)(node)) { 97 | return; 98 | } 99 | if (shouldIgnore(value, ignoreContent)) 100 | return; 101 | checkEntropy(value).forEach((payload) => { 102 | entropyReport(payload, node); 103 | }); 104 | checkRegexes(value, allPatterns).forEach((payload) => { 105 | patternReport(payload, node); 106 | }); 107 | } 108 | //Check all comments 109 | const comments = ((_a = sourceCode === null || sourceCode === void 0 ? void 0 : sourceCode.getAllComments) === null || _a === void 0 ? void 0 : _a.call(sourceCode)) || []; 110 | comments.forEach((comment) => checkString(comment.value, comment)); 111 | return { 112 | /** 113 | * For the official json 114 | */ 115 | String(node) { 116 | const { value } = node; 117 | checkString(value, node); 118 | }, 119 | Literal(node) { 120 | const { value } = node; 121 | checkString(value, node); 122 | }, 123 | TemplateElement(node) { 124 | if (!node.value) 125 | return; 126 | const value = node.value.cooked; 127 | checkString(value, node); 128 | }, 129 | JSONLiteral(node) { 130 | const { value } = node; 131 | checkString(value, node); 132 | }, 133 | }; 134 | }, 135 | }; 136 | const rules = { 137 | "no-pattern-match": no_pattern_match_1.default, 138 | "no-secrets": noSecrets, 139 | }; 140 | exports.rules = rules; 141 | -------------------------------------------------------------------------------- /dist/no-pattern-match.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const utils_1 = require("./utils"); 4 | /** 5 | * Adds a global flag to a regular expression, useful for using matchAll or just doing multiple matches 6 | * @param regexp 7 | * @returns 8 | */ 9 | function globalizeRegularExpression(regexp) { 10 | if (regexp.global) 11 | return regexp; 12 | return new RegExp(regexp, regexp.flags + "g"); 13 | } 14 | function globalizeAllRegularExps(patterns) { 15 | return Object.fromEntries(Object.entries(patterns).map(([name, pattern]) => [ 16 | name, 17 | globalizeRegularExpression(pattern), 18 | ])); 19 | } 20 | function parseAndValidateOptions({ patterns }) { 21 | const compiledRegexes = (0, utils_1.validateRecordOfRegex)((0, utils_1.plainObjectOption)(patterns, "patterns", utils_1.DEFAULT_ADDTIONAL_REGEXES)); 22 | return { 23 | patterns: compiledRegexes, 24 | }; 25 | } 26 | function findAllNewLines(text) { 27 | let pos = 0; 28 | const posistions = []; 29 | while (pos !== -1) { 30 | const nextpos = text.indexOf("\n", pos); 31 | if (nextpos === -1) 32 | break; 33 | if (nextpos === pos) 34 | pos++; 35 | posistions.push(nextpos); 36 | pos = nextpos + 1; 37 | } 38 | return posistions; 39 | } 40 | function findLineAndColNoFromMatchIdx(startIdx, linesIdx, matchLength) { 41 | const endIdx = startIdx + matchLength; 42 | const lineSelections = []; 43 | for (let i = 0; i < linesIdx.length; i++) { 44 | const lnIdx = linesIdx[i]; 45 | const lineNo = i + 1; 46 | if (startIdx <= lnIdx && (linesIdx[i - 1] || 0) <= startIdx) { 47 | //Last line 48 | if (endIdx <= lnIdx) { 49 | const endCol = endIdx - linesIdx[i - 1]; 50 | let startCol = endCol - matchLength; 51 | if (startCol < 0) { 52 | startCol = 0; 53 | } 54 | lineSelections.push({ lineNo, endCol, startCol }); 55 | return { endIdx, startIdx, lineSelections }; 56 | } 57 | else { 58 | //not last line 59 | const endCol = lnIdx - linesIdx[i - 1]; 60 | let startCol = endCol - (lnIdx - startIdx); 61 | if (startCol < 0) { 62 | startCol = 0; 63 | } 64 | lineSelections.push({ lineNo, endCol, startCol }); 65 | } 66 | } 67 | if (endIdx <= lnIdx) { 68 | const endCol = endIdx - linesIdx[i - 1]; 69 | let startCol = endCol - matchLength; 70 | if (startCol < 0) { 71 | startCol = 0; 72 | } 73 | lineSelections.push({ lineNo, endCol, startCol }); 74 | return { endIdx, startIdx, lineSelections }; 75 | } 76 | } 77 | return { endIdx, startIdx, lineSelections }; 78 | } 79 | function serializeTextSelections(textAreaSelection) { 80 | return textAreaSelection.lineSelections 81 | .map((line) => { 82 | return `${line.lineNo}:${line.startCol}-${line.endCol}`; 83 | }) 84 | .join(","); 85 | } 86 | function findStartAndEndTextSelection(textAreaSelection) { 87 | const start = { 88 | column: Infinity, 89 | line: Infinity, 90 | }; 91 | const end = { 92 | line: 0, 93 | column: 0, 94 | }; 95 | for (const line of textAreaSelection.lineSelections) { 96 | const min = Math.min(line.lineNo, start.line); 97 | if (line.lineNo === min) { 98 | start.line = min; 99 | start.column = line.startCol; 100 | } 101 | const max = Math.max(line.lineNo, end.line); 102 | if (line.lineNo === max) { 103 | end.line = max; 104 | end.column = line.endCol; 105 | } 106 | } 107 | return { 108 | start, 109 | end, 110 | }; 111 | } 112 | const FULL_TEXT_MATCH_MESSAGE = `Found text that matches the pattern "{{ patternName }}": {{ textMatch }}`; 113 | const noPatternMatch = { 114 | meta: { 115 | schema: false, 116 | messages: { 117 | [utils_1.FULL_TEXT_MATCH]: FULL_TEXT_MATCH_MESSAGE, 118 | }, 119 | docs: { 120 | description: "An eslint rule that does pattern matching against an entire file", 121 | category: "Best Practices", 122 | }, 123 | }, 124 | create(context) { 125 | var _a; 126 | const { patterns } = parseAndValidateOptions(context.options[0] || {}); 127 | const sourceCode = ((_a = context === null || context === void 0 ? void 0 : context.getSourceCode) === null || _a === void 0 ? void 0 : _a.call(context)) || context.sourceCode; 128 | const patternList = Object.entries(patterns); 129 | const text = sourceCode.text; 130 | const newLinePos = findAllNewLines(text); 131 | const matches = patternList 132 | .map(([name, pattern]) => { 133 | const globalPattern = globalizeRegularExpression(pattern); 134 | const matches = Array.from(text.matchAll(globalPattern)); 135 | return matches.map((m) => { 136 | const idx = m.index; 137 | const textMatch = m[0]; 138 | const lineAndColNumbers = findLineAndColNoFromMatchIdx(idx, newLinePos, textMatch.length); 139 | return { lineAndColNumbers, textMatch, patternName: name }; 140 | }); 141 | }) 142 | .flat(); 143 | matches.forEach(({ patternName, textMatch, lineAndColNumbers }) => { 144 | context.report({ 145 | data: { patternName, textMatch }, 146 | messageId: utils_1.FULL_TEXT_MATCH, 147 | loc: findStartAndEndTextSelection(lineAndColNumbers), 148 | }); 149 | }); 150 | return {}; 151 | }, 152 | }; 153 | exports.default = noPatternMatch; 154 | -------------------------------------------------------------------------------- /dist/regexes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | /** 4 | * From https://github.com/dxa4481/truffleHogRegexes/blob/master/truffleHogRegexes/regexes.json 5 | */ 6 | exports.default = { 7 | "Slack Token": /(xox[p|b|o|a]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32})/, 8 | "RSA private key": /-----BEGIN RSA PRIVATE KEY-----/, 9 | "SSH (OPENSSH) private key": /-----BEGIN OPENSSH PRIVATE KEY-----/, 10 | "SSH (DSA) private key": /-----BEGIN DSA PRIVATE KEY-----/, 11 | "SSH (EC) private key": /-----BEGIN EC PRIVATE KEY-----/, 12 | "PGP private key block": /-----BEGIN PGP PRIVATE KEY BLOCK-----/, 13 | "Facebook Oauth": /[f|F][a|A][c|C][e|E][b|B][o|O][o|O][k|K].*['|\"][0-9a-f]{32}['|\"]/, 14 | "Twitter Oauth": /[t|T][w|W][i|I][t|T][t|T][e|E][r|R].*['|\"][0-9a-zA-Z]{35,44}['|\"]/, 15 | GitHub: /[g|G][i|I][t|T][h|H][u|U][b|B].*['|\"][0-9a-zA-Z]{35,40}['|\"]/, 16 | "Google Oauth": /(\"client_secret\":\"[a-zA-Z0-9-_]{24}\")/, 17 | "AWS API Key": /AKIA[0-9A-Z]{16}/, 18 | "Heroku API Key": /[h|H][e|E][r|R][o|O][k|K][u|U].*[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/, 19 | "Generic Secret": /[s|S][e|E][c|C][r|R][e|E][t|T].*['|\"][0-9a-zA-Z]{32,45}['|\"]/, 20 | "Generic API Key": /[a|A][p|P][i|I][_]?[k|K][e|E][y|Y].*['|\"][0-9a-zA-Z]{32,45}['|\"]/, 21 | "Slack Webhook": /https:\/\/hooks.slack.com\/services\/T[a-zA-Z0-9_]{8}\/B[a-zA-Z0-9_]{8}\/[a-zA-Z0-9_]{24}/, 22 | "Google (GCP) Service-account": /"type": "service_account"/, 23 | "Twilio API Key": /SK[a-z0-9]{32}/, 24 | "Password in URL": /[a-zA-Z]{3,10}:\/\/[^\/\\s:@]{3,20}:[^\/\\s:@]{3,20}@.{1,100}[\"'\\s]/, 25 | }; 26 | -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.FULL_TEXT_MATCH = exports.PATTERN_MATCH = exports.HIGH_ENTROPY = exports.DEFAULT_ADDTIONAL_REGEXES = void 0; 4 | exports.isPlainObject = isPlainObject; 5 | exports.plainObjectOption = plainObjectOption; 6 | exports.validateRecordOfRegex = validateRecordOfRegex; 7 | exports.checkOptions = checkOptions; 8 | exports.shannonEntropy = shannonEntropy; 9 | exports.isModulePathString = isModulePathString; 10 | exports.getIdentifierName = getIdentifierName; 11 | const MATH_LOG_2 = Math.log(2); 12 | /** 13 | * Charset especially designed to ignore common regular expressions (eg [] and {}), imports/requires (/.), and css classes (-), and other special characters, 14 | * which raise a lot of false postives and aren't usually in passwords/secrets 15 | */ 16 | const CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=!|*^@~`$%+?\"'_<>".split(""); 17 | const DEFAULT_TOLERANCE = 4; 18 | exports.DEFAULT_ADDTIONAL_REGEXES = {}; 19 | function isPlainObject(obj) { 20 | return typeof obj === "object" && obj.constructor === Object; 21 | } 22 | function compileListOfPatterns(patterns = [], name) { 23 | if (!Array.isArray(patterns)) { 24 | if (typeof patterns === "string" || patterns instanceof RegExp) { 25 | patterns = [patterns]; 26 | } 27 | else { 28 | throw new Error(`Expected '${name}' to be an a array, a string, or a RegExp`); 29 | } 30 | } 31 | const compiledPatterns = []; 32 | for (let i = 0; i < patterns.length; i++) { 33 | try { 34 | const pattern = patterns[i]; 35 | compiledPatterns[i] = 36 | pattern instanceof RegExp ? pattern : new RegExp(String(pattern)); 37 | } 38 | catch (e) { 39 | throw new Error("Failed to compiled the regexp " + patterns[i]); 40 | } 41 | } 42 | return compiledPatterns; 43 | } 44 | function booleanOption(value, name, defaultValue) { 45 | //TODO: This is kind of ridiclous check, fix this 46 | value = value || defaultValue; 47 | if (typeof value !== "boolean") { 48 | throw new Error(`The option '${name}' must be boolean`); 49 | } 50 | return value; 51 | } 52 | function plainObjectOption(value, name, defaultValue) { 53 | value = value || defaultValue; 54 | if (!isPlainObject(value)) { 55 | throw new Error(`The option '${name}' must be a plain object`); 56 | } 57 | return value; 58 | } 59 | function validateRecordOfRegex(recordOfRegex) { 60 | const compiledRegexes = {}; 61 | for (const regexName in recordOfRegex) { 62 | if (recordOfRegex.hasOwnProperty(regexName)) { 63 | try { 64 | compiledRegexes[regexName] = 65 | recordOfRegex[regexName] instanceof RegExp 66 | ? recordOfRegex[regexName] 67 | : new RegExp(String(recordOfRegex[regexName])); 68 | } 69 | catch (e) { 70 | throw new Error("Could not compile the regexp " + 71 | regexName + 72 | " with the value " + 73 | recordOfRegex[regexName]); 74 | } 75 | } 76 | } 77 | return compiledRegexes; 78 | } 79 | function checkOptions({ tolerance, additionalRegexes, ignoreContent, ignoreModules, ignoreIdentifiers, additionalDelimiters, ignoreCase, }) { 80 | ignoreModules = booleanOption(ignoreModules, "ignoreModules", true); 81 | ignoreCase = booleanOption(ignoreCase, "ignoreCase", false); 82 | tolerance = tolerance || DEFAULT_TOLERANCE; 83 | if (typeof tolerance !== "number" || tolerance <= 0) { 84 | throw new Error("The option tolerance must be a positive (eg greater than zero) number"); 85 | } 86 | additionalRegexes = plainObjectOption(additionalRegexes, "additionalRegexes", exports.DEFAULT_ADDTIONAL_REGEXES); 87 | const compiledRegexes = validateRecordOfRegex(additionalRegexes); 88 | return { 89 | tolerance, 90 | additionalRegexes: compiledRegexes, 91 | ignoreContent: compileListOfPatterns(ignoreContent), 92 | ignoreModules, 93 | ignoreIdentifiers: compileListOfPatterns(ignoreIdentifiers), 94 | additionalDelimiters: compileListOfPatterns(additionalDelimiters), 95 | ignoreCase, 96 | }; 97 | } 98 | /** 99 | * From https://github.com/dxa4481/truffleHog/blob/dev/truffleHog/truffleHog.py#L85 100 | * @param {*} str 101 | */ 102 | function shannonEntropy(str) { 103 | if (!str) 104 | return 0; 105 | let entropy = 0; 106 | const len = str.length; 107 | for (let i = 0; i < CHARSET.length; ++i) { 108 | //apparently this is the fastest way to char count in js 109 | const ratio = (str.split(CHARSET[i]).length - 1) / len; 110 | if (ratio > 0) 111 | entropy += -(ratio * (Math.log(ratio) / MATH_LOG_2)); 112 | } 113 | return entropy; 114 | } 115 | const MODULE_FUNCTIONS = ["import", "require"]; 116 | /** 117 | * Used to detect "import()" and "require()" 118 | * Inspired by https://github.com/benmosher/eslint-plugin-import/blob/45bfe472f38ef790c11efe45ffc59808c67a3f94/src/core/staticRequire.js 119 | * @param {*} node 120 | */ 121 | function isStaticImportOrRequire(node) { 122 | return (node && 123 | node.callee && 124 | node.callee.type === "Identifier" && 125 | MODULE_FUNCTIONS.indexOf(node.callee.name) !== -1 && 126 | node.arguments.length === 1 && 127 | node.arguments[0].type === "Literal" && 128 | typeof node.arguments[0].value === "string"); 129 | } 130 | function isImportString(node) { 131 | return node && node.parent && node.parent.type === "ImportDeclaration"; 132 | } 133 | function isModulePathString(node) { 134 | return isStaticImportOrRequire(node.parent) || isImportString(node) || false; 135 | } 136 | const VARORPROP = ["AssignmentExpression", "Property", "VariableDeclarator"]; 137 | function getPropertyName(node) { 138 | return (node.parent.key && 139 | node.parent.key.type === "Identifier" && 140 | node.parent.key.name); 141 | } 142 | function getIdentifierName(node) { 143 | if (!node || !node.parent) 144 | return false; 145 | switch (node.parent.type) { 146 | case "VariableDeclarator": 147 | return getVarName(node); 148 | case "AssignmentExpression": 149 | return getAssignmentName(node); 150 | case "Property": 151 | return getPropertyName(node); 152 | default: 153 | return false; 154 | } 155 | } 156 | function getVarName(node) { 157 | return node.parent.id && node.parent.id.name; 158 | } 159 | function getAssignmentName(node) { 160 | return (node.parent.left && 161 | node.parent.property && 162 | node.parent.property.type === "Identifier" && 163 | node.parent.property.name); 164 | } 165 | exports.HIGH_ENTROPY = "HIGH_ENTROPY"; 166 | exports.PATTERN_MATCH = "PATTERN_MATCH"; 167 | exports.FULL_TEXT_MATCH = "FULL_TEXT_MATCH"; 168 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type { ESLint, Rule } from "eslint"; 2 | 3 | declare const eslintPluginNoSecrets: ESLint.Plugin & { 4 | rules: { 5 | "no-secrets": Rule.RuleModule; 6 | "no-pattern-match": Rule.RuleModule; 7 | }; 8 | }; 9 | 10 | export = eslintPluginNoSecrets; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-no-secrets", 3 | "version": "2.2.1", 4 | "description": "An eslint rule that searches for potential secrets/keys in code", 5 | "main": "./dist/index.js", 6 | "types": "./index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "npm run test:unit && npm run test:staging", 10 | "test:unit": "ts-node tests/lib/rules", 11 | "test:staging": "npm run build && node ./staging/staging.spec.js" 12 | }, 13 | "keywords": [ 14 | "eslint", 15 | "eslint-plugin", 16 | "security", 17 | "secure", 18 | "secrets", 19 | "lint", 20 | "eslintplugin" 21 | ], 22 | "author": "Nick Deis ", 23 | "repository": "https://github.com/nickdeis/eslint-plugin-no-secrets", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@eslint/json": "^0.10.0", 27 | "@types/eslint": "^9.6.1", 28 | "@types/node": "^22.9.1", 29 | "eslint": "^7.19.0", 30 | "eslint-plugin-jsonc": "^2.15.1", 31 | "eslint-plugin-self": "^1.2.0", 32 | "eslint6": "npm:eslint@^6.8.0", 33 | "eslint8": "npm:eslint@^8.57.0", 34 | "eslint9": "npm:eslint@^9.19.0", 35 | "ts-node": "^10.9.2", 36 | "typescript": "^5.6.3" 37 | }, 38 | "peerDependencies": { 39 | "eslint": ">=5" 40 | }, 41 | "engines": { 42 | "npm": ">=8", 43 | "node": ">=18" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Rule, ESLint } from "eslint"; 2 | import { 3 | getIdentifierName, 4 | shannonEntropy, 5 | checkOptions, 6 | HIGH_ENTROPY, 7 | PATTERN_MATCH, 8 | isModulePathString, 9 | } from "./utils"; 10 | import STANDARD_PATTERNS from "./regexes"; 11 | import noPatternMatch from "./no-pattern-match"; 12 | 13 | type Literal = string | number | bigint | boolean | RegExp; 14 | 15 | function isNonEmptyString(value: Literal): value is string { 16 | return !!(value && typeof value === "string"); 17 | } 18 | 19 | function checkRegexes(value: string, patterns: Record) { 20 | return Object.keys(patterns) 21 | .map((name) => { 22 | const pattern = patterns[name]; 23 | const m = value.match(pattern); 24 | if (!m || !m[0]) return m; 25 | return { name, match: m[0] }; 26 | }) 27 | .filter((payload) => !!payload); 28 | } 29 | 30 | function shouldIgnore(value: string, toIgnore) { 31 | for (let i = 0; i < toIgnore.length; i++) { 32 | if (value.match(toIgnore[i])) return true; 33 | } 34 | return false; 35 | } 36 | 37 | const meta: ESLint.Plugin["meta"] = { 38 | name: "eslint-plugin-no-secrets", 39 | version: "2.1.1", 40 | }; 41 | 42 | const noSecrets: Rule.RuleModule = { 43 | meta: { 44 | schema: false, 45 | messages: { 46 | [HIGH_ENTROPY]: `Found a string with entropy {{ entropy }} : "{{ token }}"`, 47 | [PATTERN_MATCH]: `Found a string that matches "{{ name }}" : "{{ match }}"`, 48 | }, 49 | docs: { 50 | description: 51 | "An eslint rule that looks for possible leftover secrets in code", 52 | category: "Best Practices", 53 | }, 54 | }, 55 | create(context) { 56 | const { 57 | tolerance, 58 | additionalRegexes, 59 | ignoreContent, 60 | ignoreModules, 61 | ignoreIdentifiers, 62 | additionalDelimiters, 63 | ignoreCase, 64 | } = checkOptions(context.options[0] || {}); 65 | const sourceCode = context.getSourceCode() || context.sourceCode; 66 | 67 | const allPatterns = Object.assign({}, STANDARD_PATTERNS, additionalRegexes); 68 | 69 | const allDelimiters: (string | RegExp)[] = ( 70 | additionalDelimiters as (string | RegExp)[] 71 | ).concat([" "]); 72 | 73 | function splitIntoTokens(value: string) { 74 | let tokens = [value]; 75 | allDelimiters.forEach((delimiter) => { 76 | //@ts-ignore 77 | tokens = tokens.map((token) => token.split(delimiter)); 78 | //flatten 79 | tokens = [].concat.apply([], tokens); 80 | }); 81 | return tokens; 82 | } 83 | 84 | function checkEntropy(value: string) { 85 | value = ignoreCase ? value.toLowerCase() : value; 86 | const tokens = splitIntoTokens(value); 87 | return tokens 88 | .map((token) => { 89 | const entropy = shannonEntropy(token); 90 | return { token, entropy }; 91 | }) 92 | .filter((payload) => tolerance <= payload.entropy); 93 | } 94 | 95 | function entropyReport(data, node) { 96 | //Easier to read numbers 97 | data.entropy = Math.round(data.entropy * 100) / 100; 98 | context.report({ 99 | node, 100 | data, 101 | messageId: HIGH_ENTROPY, 102 | }); 103 | } 104 | 105 | function patternReport(data, node) { 106 | context.report({ 107 | node, 108 | data, 109 | messageId: PATTERN_MATCH, 110 | }); 111 | } 112 | function checkString(value: Literal, node) { 113 | const idName = getIdentifierName(node); 114 | if (idName && shouldIgnore(idName, ignoreIdentifiers)) return; 115 | if (!isNonEmptyString(value)) return; 116 | if (ignoreModules && isModulePathString(node)) { 117 | return; 118 | } 119 | if (shouldIgnore(value, ignoreContent)) return; 120 | checkEntropy(value).forEach((payload) => { 121 | entropyReport(payload, node); 122 | }); 123 | checkRegexes(value, allPatterns).forEach((payload) => { 124 | patternReport(payload, node); 125 | }); 126 | } 127 | 128 | //Check all comments 129 | const comments = sourceCode?.getAllComments?.() || []; 130 | comments.forEach((comment) => checkString(comment.value, comment)); 131 | 132 | return { 133 | /** 134 | * For the official eslint json plugin 135 | */ 136 | String(node) { 137 | const { value } = node; 138 | checkString(value, node); 139 | }, 140 | Literal(node) { 141 | const { value } = node; 142 | checkString(value, node); 143 | }, 144 | TemplateElement(node) { 145 | if (!node.value) return; 146 | const value = node.value.cooked; 147 | checkString(value, node); 148 | }, 149 | JSONLiteral(node) { 150 | const { value } = node; 151 | checkString(value, node); 152 | }, 153 | }; 154 | }, 155 | }; 156 | 157 | const rules = { 158 | "no-pattern-match": noPatternMatch, 159 | "no-secrets": noSecrets, 160 | }; 161 | 162 | export { meta, rules }; 163 | -------------------------------------------------------------------------------- /src/no-pattern-match.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from "eslint"; 2 | import { 3 | DEFAULT_ADDTIONAL_REGEXES, 4 | FULL_TEXT_MATCH, 5 | plainObjectOption, 6 | validateRecordOfRegex, 7 | } from "./utils"; 8 | 9 | /** 10 | * Adds a global flag to a regular expression, useful for using matchAll or just doing multiple matches 11 | * @param regexp 12 | * @returns 13 | */ 14 | function globalizeRegularExpression(regexp: RegExp) { 15 | if (regexp.global) return regexp; 16 | return new RegExp(regexp, regexp.flags + "g"); 17 | } 18 | 19 | function globalizeAllRegularExps( 20 | patterns: Record 21 | ): Record { 22 | return Object.fromEntries( 23 | Object.entries(patterns).map(([name, pattern]) => [ 24 | name, 25 | globalizeRegularExpression(pattern), 26 | ]) 27 | ); 28 | } 29 | 30 | function parseAndValidateOptions({ patterns }) { 31 | const compiledRegexes = validateRecordOfRegex( 32 | plainObjectOption(patterns, "patterns", DEFAULT_ADDTIONAL_REGEXES) 33 | ); 34 | return { 35 | patterns: compiledRegexes, 36 | }; 37 | } 38 | 39 | function findAllNewLines(text: string) { 40 | let pos = 0; 41 | const posistions: number[] = []; 42 | while (pos !== -1) { 43 | const nextpos = text.indexOf("\n", pos); 44 | if (nextpos === -1) break; 45 | if (nextpos === pos) pos++; 46 | posistions.push(nextpos); 47 | pos = nextpos + 1; 48 | } 49 | return posistions; 50 | } 51 | 52 | type LineTextArea = { 53 | lineNo: number; 54 | startCol: number; 55 | endCol: number; 56 | }; 57 | 58 | type TextAreaSelection = { 59 | startIdx: number; 60 | endIdx: number; 61 | lineSelections: LineTextArea[]; 62 | }; 63 | 64 | function findLineAndColNoFromMatchIdx( 65 | startIdx: number, 66 | linesIdx: number[], 67 | matchLength: number 68 | ): TextAreaSelection { 69 | const endIdx = startIdx + matchLength; 70 | const lineSelections: LineTextArea[] = []; 71 | for (let i = 0; i < linesIdx.length; i++) { 72 | const lnIdx = linesIdx[i]; 73 | const lineNo = i + 1; 74 | if (startIdx <= lnIdx && (linesIdx[i - 1] || 0) <= startIdx) { 75 | //Last line 76 | if (endIdx <= lnIdx) { 77 | const endCol = endIdx - linesIdx[i - 1]; 78 | let startCol = endCol - matchLength; 79 | if (startCol < 0) { 80 | startCol = 0; 81 | } 82 | lineSelections.push({ lineNo, endCol, startCol }); 83 | return { endIdx, startIdx, lineSelections }; 84 | } else { 85 | //not last line 86 | const endCol = lnIdx - linesIdx[i - 1]; 87 | let startCol = endCol - (lnIdx - startIdx); 88 | if (startCol < 0) { 89 | startCol = 0; 90 | } 91 | lineSelections.push({ lineNo, endCol, startCol }); 92 | } 93 | } 94 | if (endIdx <= lnIdx) { 95 | const endCol = endIdx - linesIdx[i - 1]; 96 | let startCol = endCol - matchLength; 97 | if (startCol < 0) { 98 | startCol = 0; 99 | } 100 | lineSelections.push({ lineNo, endCol, startCol }); 101 | return { endIdx, startIdx, lineSelections }; 102 | } 103 | } 104 | return { endIdx, startIdx, lineSelections }; 105 | } 106 | 107 | function serializeTextSelections(textAreaSelection: TextAreaSelection) { 108 | return textAreaSelection.lineSelections 109 | .map((line) => { 110 | return `${line.lineNo}:${line.startCol}-${line.endCol}`; 111 | }) 112 | .join(","); 113 | } 114 | 115 | function findStartAndEndTextSelection(textAreaSelection: TextAreaSelection) { 116 | const start = { 117 | column: Infinity, 118 | line: Infinity, 119 | }; 120 | const end = { 121 | line: 0, 122 | column: 0, 123 | }; 124 | for (const line of textAreaSelection.lineSelections) { 125 | const min = Math.min(line.lineNo, start.line); 126 | if (line.lineNo === min) { 127 | start.line = min; 128 | start.column = line.startCol; 129 | } 130 | const max = Math.max(line.lineNo, end.line); 131 | if (line.lineNo === max) { 132 | end.line = max; 133 | end.column = line.endCol; 134 | } 135 | } 136 | return { 137 | start, 138 | end, 139 | }; 140 | } 141 | 142 | const FULL_TEXT_MATCH_MESSAGE = `Found text that matches the pattern "{{ patternName }}": {{ textMatch }}`; 143 | 144 | const noPatternMatch: Rule.RuleModule = { 145 | meta: { 146 | schema: false, 147 | messages: { 148 | [FULL_TEXT_MATCH]: FULL_TEXT_MATCH_MESSAGE, 149 | }, 150 | docs: { 151 | description: 152 | "An eslint rule that does pattern matching against an entire file", 153 | category: "Best Practices", 154 | }, 155 | }, 156 | create(context) { 157 | const { patterns } = parseAndValidateOptions(context.options[0] || {}); 158 | const sourceCode = context?.getSourceCode?.() || context.sourceCode; 159 | const patternList = Object.entries(patterns); 160 | const text = sourceCode.text; 161 | const newLinePos = findAllNewLines(text); 162 | const matches = patternList 163 | .map(([name, pattern]) => { 164 | const globalPattern = globalizeRegularExpression(pattern); 165 | const matches = Array.from(text.matchAll(globalPattern)); 166 | return matches.map((m) => { 167 | const idx = m.index; 168 | const textMatch = m[0]; 169 | const lineAndColNumbers = findLineAndColNoFromMatchIdx( 170 | idx, 171 | newLinePos, 172 | textMatch.length 173 | ); 174 | 175 | return { lineAndColNumbers, textMatch, patternName: name }; 176 | }); 177 | }) 178 | .flat(); 179 | matches.forEach(({ patternName, textMatch, lineAndColNumbers }) => { 180 | context.report({ 181 | data: { patternName, textMatch }, 182 | messageId: FULL_TEXT_MATCH, 183 | loc: findStartAndEndTextSelection(lineAndColNumbers), 184 | }); 185 | }); 186 | return {}; 187 | }, 188 | }; 189 | 190 | export default noPatternMatch; 191 | -------------------------------------------------------------------------------- /src/regexes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * From https://github.com/dxa4481/truffleHogRegexes/blob/master/truffleHogRegexes/regexes.json 3 | */ 4 | export default { 5 | "Slack Token": /(xox[p|b|o|a]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32})/, 6 | "RSA private key": /-----BEGIN RSA PRIVATE KEY-----/, 7 | "SSH (OPENSSH) private key": /-----BEGIN OPENSSH PRIVATE KEY-----/, 8 | "SSH (DSA) private key": /-----BEGIN DSA PRIVATE KEY-----/, 9 | "SSH (EC) private key": /-----BEGIN EC PRIVATE KEY-----/, 10 | "PGP private key block": /-----BEGIN PGP PRIVATE KEY BLOCK-----/, 11 | "Facebook Oauth": 12 | /[f|F][a|A][c|C][e|E][b|B][o|O][o|O][k|K].*['|\"][0-9a-f]{32}['|\"]/, 13 | "Twitter Oauth": 14 | /[t|T][w|W][i|I][t|T][t|T][e|E][r|R].*['|\"][0-9a-zA-Z]{35,44}['|\"]/, 15 | GitHub: /[g|G][i|I][t|T][h|H][u|U][b|B].*['|\"][0-9a-zA-Z]{35,40}['|\"]/, 16 | "Google Oauth": /(\"client_secret\":\"[a-zA-Z0-9-_]{24}\")/, 17 | "AWS API Key": /AKIA[0-9A-Z]{16}/, 18 | "Heroku API Key": 19 | /[h|H][e|E][r|R][o|O][k|K][u|U].*[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/, 20 | "Generic Secret": 21 | /[s|S][e|E][c|C][r|R][e|E][t|T].*['|\"][0-9a-zA-Z]{32,45}['|\"]/, 22 | "Generic API Key": 23 | /[a|A][p|P][i|I][_]?[k|K][e|E][y|Y].*['|\"][0-9a-zA-Z]{32,45}['|\"]/, 24 | "Slack Webhook": 25 | /https:\/\/hooks.slack.com\/services\/T[a-zA-Z0-9_]{8}\/B[a-zA-Z0-9_]{8}\/[a-zA-Z0-9_]{24}/, 26 | "Google (GCP) Service-account": /"type": "service_account"/, 27 | "Twilio API Key": /SK[a-z0-9]{32}/, 28 | "Password in URL": 29 | /[a-zA-Z]{3,10}:\/\/[^\/\\s:@]{3,20}:[^\/\\s:@]{3,20}@.{1,100}[\"'\\s]/, 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | const MATH_LOG_2 = Math.log(2); 2 | /** 3 | * Charset especially designed to ignore common regular expressions (eg [] and {}), imports/requires (/.), and css classes (-), and other special characters, 4 | * which raise a lot of false postives and aren't usually in passwords/secrets 5 | */ 6 | const CHARSET = 7 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=!|*^@~`$%+?\"'_<>".split( 8 | "" 9 | ); 10 | const DEFAULT_TOLERANCE = 4; 11 | export const DEFAULT_ADDTIONAL_REGEXES = {}; 12 | 13 | type PlainObject = { 14 | [key: string | number]: any; 15 | }; 16 | 17 | export function isPlainObject(obj: any): obj is PlainObject { 18 | return typeof obj === "object" && obj.constructor === Object; 19 | } 20 | 21 | function compileListOfPatterns( 22 | patterns: string | RegExp | (string | RegExp)[] = [], 23 | name?: string 24 | ) { 25 | if (!Array.isArray(patterns)) { 26 | if (typeof patterns === "string" || patterns instanceof RegExp) { 27 | patterns = [patterns]; 28 | } else { 29 | throw new Error( 30 | `Expected '${name}' to be an a array, a string, or a RegExp` 31 | ); 32 | } 33 | } 34 | 35 | const compiledPatterns: RegExp[] = []; 36 | for (let i = 0; i < patterns.length; i++) { 37 | try { 38 | const pattern = patterns[i]; 39 | compiledPatterns[i] = 40 | pattern instanceof RegExp ? pattern : new RegExp(String(pattern)); 41 | } catch (e) { 42 | throw new Error("Failed to compiled the regexp " + patterns[i]); 43 | } 44 | } 45 | return compiledPatterns; 46 | } 47 | 48 | function booleanOption(value: any, name: string, defaultValue: any): boolean { 49 | //TODO: This is kind of ridiclous check, fix this 50 | value = value || defaultValue; 51 | if (typeof value !== "boolean") { 52 | throw new Error(`The option '${name}' must be boolean`); 53 | } 54 | return value; 55 | } 56 | 57 | export function plainObjectOption( 58 | value: any, 59 | name: string, 60 | defaultValue: PlainObject 61 | ) { 62 | value = value || defaultValue; 63 | if (!isPlainObject(value)) { 64 | throw new Error(`The option '${name}' must be a plain object`); 65 | } 66 | return value; 67 | } 68 | 69 | export function validateRecordOfRegex(recordOfRegex: PlainObject) { 70 | const compiledRegexes: Record = {}; 71 | for (const regexName in recordOfRegex) { 72 | if (recordOfRegex.hasOwnProperty(regexName)) { 73 | try { 74 | compiledRegexes[regexName] = 75 | recordOfRegex[regexName] instanceof RegExp 76 | ? recordOfRegex[regexName] 77 | : new RegExp(String(recordOfRegex[regexName])); 78 | } catch (e) { 79 | throw new Error( 80 | "Could not compile the regexp " + 81 | regexName + 82 | " with the value " + 83 | recordOfRegex[regexName] 84 | ); 85 | } 86 | } 87 | } 88 | return compiledRegexes; 89 | } 90 | 91 | export function checkOptions({ 92 | tolerance, 93 | additionalRegexes, 94 | ignoreContent, 95 | ignoreModules, 96 | ignoreIdentifiers, 97 | additionalDelimiters, 98 | ignoreCase, 99 | }) { 100 | ignoreModules = booleanOption(ignoreModules, "ignoreModules", true); 101 | ignoreCase = booleanOption(ignoreCase, "ignoreCase", false); 102 | tolerance = tolerance || DEFAULT_TOLERANCE; 103 | if (typeof tolerance !== "number" || tolerance <= 0) { 104 | throw new Error( 105 | "The option tolerance must be a positive (eg greater than zero) number" 106 | ); 107 | } 108 | additionalRegexes = plainObjectOption( 109 | additionalRegexes, 110 | "additionalRegexes", 111 | DEFAULT_ADDTIONAL_REGEXES 112 | ); 113 | 114 | const compiledRegexes = validateRecordOfRegex(additionalRegexes); 115 | 116 | return { 117 | tolerance, 118 | additionalRegexes: compiledRegexes, 119 | ignoreContent: compileListOfPatterns(ignoreContent), 120 | ignoreModules, 121 | ignoreIdentifiers: compileListOfPatterns(ignoreIdentifiers), 122 | additionalDelimiters: compileListOfPatterns(additionalDelimiters), 123 | ignoreCase, 124 | }; 125 | } 126 | 127 | /** 128 | * From https://github.com/dxa4481/truffleHog/blob/dev/truffleHog/truffleHog.py#L85 129 | * @param {*} str 130 | */ 131 | export function shannonEntropy(str: string) { 132 | if (!str) return 0; 133 | let entropy = 0; 134 | const len = str.length; 135 | for (let i = 0; i < CHARSET.length; ++i) { 136 | //apparently this is the fastest way to char count in js 137 | const ratio = (str.split(CHARSET[i]).length - 1) / len; 138 | if (ratio > 0) entropy += -(ratio * (Math.log(ratio) / MATH_LOG_2)); 139 | } 140 | return entropy; 141 | } 142 | 143 | const MODULE_FUNCTIONS = ["import", "require"]; 144 | 145 | /** 146 | * Used to detect "import()" and "require()" 147 | * Inspired by https://github.com/benmosher/eslint-plugin-import/blob/45bfe472f38ef790c11efe45ffc59808c67a3f94/src/core/staticRequire.js 148 | * @param {*} node 149 | */ 150 | function isStaticImportOrRequire(node): boolean { 151 | return ( 152 | node && 153 | node.callee && 154 | node.callee.type === "Identifier" && 155 | MODULE_FUNCTIONS.indexOf(node.callee.name) !== -1 && 156 | node.arguments.length === 1 && 157 | node.arguments[0].type === "Literal" && 158 | typeof node.arguments[0].value === "string" 159 | ); 160 | } 161 | 162 | function isImportString(node) { 163 | return node && node.parent && node.parent.type === "ImportDeclaration"; 164 | } 165 | 166 | export function isModulePathString(node) { 167 | return isStaticImportOrRequire(node.parent) || isImportString(node) || false; 168 | } 169 | 170 | const VARORPROP = ["AssignmentExpression", "Property", "VariableDeclarator"]; 171 | 172 | function getPropertyName(node) { 173 | return ( 174 | node.parent.key && 175 | node.parent.key.type === "Identifier" && 176 | node.parent.key.name 177 | ); 178 | } 179 | 180 | export function getIdentifierName(node): false | string { 181 | if (!node || !node.parent) return false; 182 | switch (node.parent.type) { 183 | case "VariableDeclarator": 184 | return getVarName(node); 185 | case "AssignmentExpression": 186 | return getAssignmentName(node); 187 | case "Property": 188 | return getPropertyName(node); 189 | default: 190 | return false; 191 | } 192 | } 193 | 194 | function getVarName(node): string { 195 | return node.parent.id && node.parent.id.name; 196 | } 197 | 198 | function getAssignmentName(node) { 199 | return ( 200 | node.parent.left && 201 | node.parent.property && 202 | node.parent.property.type === "Identifier" && 203 | node.parent.property.name 204 | ); 205 | } 206 | 207 | export const HIGH_ENTROPY = "HIGH_ENTROPY"; 208 | 209 | export const PATTERN_MATCH = "PATTERN_MATCH"; 210 | 211 | export const FULL_TEXT_MATCH = "FULL_TEXT_MATCH"; 212 | -------------------------------------------------------------------------------- /staging/has-no-secret.js: -------------------------------------------------------------------------------- 1 | const NOT_A_SECRET = "Hopefully not a secret"; -------------------------------------------------------------------------------- /staging/has-no-secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "NOT_A_SECRET":"Hopefully not a secret", 3 | "NOT_A_SECRET_ARRAY":["No secrets here"] 4 | } -------------------------------------------------------------------------------- /staging/has-secret.js: -------------------------------------------------------------------------------- 1 | const SECRET = "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F"; -------------------------------------------------------------------------------- /staging/has-secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "SECRET":"AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F" 3 | } -------------------------------------------------------------------------------- /staging/json-flat.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test for official ESLint JSON plugin 3 | */ 4 | const json = require("@eslint/json").default; 5 | const noSecret = require("../dist"); 6 | 7 | module.exports = [ 8 | // lint JSON files 9 | { 10 | plugins: { 11 | json, 12 | "no-secrets": noSecret, 13 | }, 14 | }, 15 | 16 | // lint JSON files 17 | { 18 | files: ["**/*.json"], 19 | language: "json/json", 20 | rules: { 21 | "no-secrets/no-secrets": "error", 22 | "no-secrets/no-pattern-match": [ 23 | "error", 24 | { patterns: { SecretJS: /const SECRET/, SecretJSON: /\"SECRET\"/ } }, 25 | ], 26 | }, 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /staging/jsonc-flat.eslintrc.js: -------------------------------------------------------------------------------- 1 | const noSecret = require("../dist"); 2 | const jsoncExtend = require("eslint-plugin-jsonc"); 3 | 4 | module.exports = [ 5 | ...jsoncExtend.configs["flat/recommended-with-jsonc"], 6 | { 7 | languageOptions: { ecmaVersion: 6 }, 8 | plugins: { 9 | "no-secrets": noSecret, 10 | }, 11 | rules: { 12 | "no-secrets/no-secrets": "error", 13 | "no-secrets/no-pattern-match": [ 14 | "error", 15 | { patterns: { SecretJS: /const SECRET/, SecretJSON: /\"SECRET\"/ } }, 16 | ], 17 | }, 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /staging/jsonc.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["plugin:jsonc/base"], 3 | plugins: ["self"], 4 | rules: { 5 | "self/no-secrets": "error", 6 | "self/no-pattern-match": [ 7 | "error", 8 | { patterns: { SecretJS: /const SECRET/, SecretJSON: /\"SECRET\"/ } }, 9 | ], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /staging/mixed-flat.eslintrc.js: -------------------------------------------------------------------------------- 1 | const noSecret = require("../dist"); 2 | const jsoncExtend = require("eslint-plugin-jsonc"); 3 | 4 | module.exports = [ 5 | ...jsoncExtend.configs["flat/recommended-with-jsonc"], 6 | { 7 | languageOptions: { ecmaVersion: 6 }, 8 | plugins: { 9 | "no-secrets": noSecret, 10 | }, 11 | rules: { 12 | "no-secrets/no-secrets": "error", 13 | "no-secrets/no-pattern-match": [ 14 | "error", 15 | { patterns: { SecretJS: /const SECRET/, SecretJSON: /\"SECRET\"/ } }, 16 | ], 17 | }, 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /staging/mixed.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { es6: true }, 3 | extends: ["plugin:jsonc/base"], 4 | plugins: ["self"], 5 | rules: { 6 | "self/no-secrets": "error", 7 | "self/no-pattern-match": [ 8 | "error", 9 | { patterns: { SecretJS: /const SECRET/, SecretJSON: /\"SECRET\"/ } }, 10 | ], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /staging/normal-flat.eslintrc.js: -------------------------------------------------------------------------------- 1 | const noSecret = require("../dist"); 2 | module.exports = { 3 | languageOptions: { ecmaVersion: 6 }, 4 | plugins: { 5 | "no-secrets": noSecret, 6 | }, 7 | rules: { 8 | "no-secrets/no-secrets": "error", 9 | "no-secrets/no-pattern-match": [ 10 | "error", 11 | { patterns: { SecretJS: /const SECRET/, SecretJSON: /\"SECRET\"/ } }, 12 | ], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /staging/normal.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { es6: true }, 3 | plugins: ["self"], 4 | rules: { 5 | "self/no-secrets": "error", 6 | "self/no-pattern-match": [ 7 | "error", 8 | { patterns: { SecretJS: /const SECRET/, SecretJSON: /\"SECRET\"/ } }, 9 | ], 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /staging/staging.spec.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const ESLint = require("eslint").ESLint; 3 | const ESLint9 = require("eslint9").ESLint; 4 | const assert = require("assert"); 5 | const { describe, it } = require("node:test"); 6 | 7 | const JSON_FILES = [ 8 | { 9 | name: "Should not detect non-secrets", 10 | file: "./staging/has-no-secret.json", 11 | errorCount: 0, 12 | }, 13 | { 14 | name: "Should detect secrets in json files", 15 | file: "./staging/has-secret.json", 16 | errorCount: 2, 17 | }, 18 | ]; 19 | 20 | const JS_FILES = [ 21 | { 22 | name: "Should not detect non-secrets", 23 | file: "./staging/has-no-secret.js", 24 | errorCount: 0, 25 | }, 26 | { 27 | name: "Should detect secrets in json files", 28 | file: "./staging/has-secret.js", 29 | errorCount: 2, 30 | }, 31 | ]; 32 | 33 | const TESTS = { 34 | "jsonc.eslintrc.js": JSON_FILES, 35 | "normal.eslintrc.js": JS_FILES, 36 | "mixed.eslintrc.js": [].concat(JSON_FILES).concat(JS_FILES), 37 | }; 38 | 39 | const FLAT_TESTS = { 40 | "jsonc-flat.eslintrc.js": JSON_FILES, 41 | "normal-flat.eslintrc.js": JS_FILES, 42 | "mixed-flat.eslintrc.js": [].concat(JSON_FILES).concat(JS_FILES), 43 | "json-flat.eslintrc.js": JSON_FILES, 44 | }; 45 | 46 | async function runTests(tests, eslintClazz) { 47 | const configs = Object.entries(tests); 48 | for (const [config, tests] of configs) { 49 | const eslint = new eslintClazz({ 50 | overrideConfigFile: path.join(__dirname, config), 51 | }); 52 | const files = tests.map((test) => test.file); 53 | console.log(config); 54 | const results = await eslint.lintFiles(files); 55 | 56 | describe(config, () => { 57 | for (let i = 0; i < tests.length; i++) { 58 | const test = tests[i]; 59 | const report = results[i]; 60 | it(test.name, () => { 61 | assert.strictEqual( 62 | test.errorCount, 63 | report.errorCount, 64 | JSON.stringify(report.messages, null, 2) 65 | ); 66 | }); 67 | } 68 | }); 69 | } 70 | } 71 | 72 | describe("Staging tests", async () => { 73 | describe("JSON compat testing", async () => { 74 | await runTests(TESTS, ESLint); 75 | }); 76 | 77 | describe("Flat config testing", async () => { 78 | await runTests(FLAT_TESTS, ESLint9); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /tests/lib/rules/index.ts: -------------------------------------------------------------------------------- 1 | import "./no-secrets"; 2 | import "./no-pattern-match"; 3 | -------------------------------------------------------------------------------- /tests/lib/rules/no-pattern-match.ts: -------------------------------------------------------------------------------- 1 | import { rules } from "../../../src/index"; 2 | import { FULL_TEXT_MATCH } from "../../../src/utils"; 3 | import RULE_TESTERS from "./rule-testers"; 4 | const noPatternMatch = rules["no-pattern-match"]; 5 | 6 | const FULL_TEXT_NO_SECRETS = ` 7 | /**Not a problem**/ 8 | const A = "Not a problem"; 9 | `; 10 | 11 | const FULL_TEXT_SECRETS = ` 12 | /**SECRET**/ 13 | const VAULT = { 14 | token:"secret secret SECRET" 15 | }; 16 | `; 17 | 18 | const patterns = { 19 | Test: /secret/i, 20 | MultiLine: /VAULT = {[\n.\s\t]*to/im, 21 | }; 22 | 23 | const FULL_TEXT_MATCH_MSG = { 24 | messageId: FULL_TEXT_MATCH, 25 | }; 26 | 27 | function createTests(_flatConfig = false) { 28 | return { 29 | valid: [ 30 | { 31 | code: FULL_TEXT_NO_SECRETS, 32 | options: [ 33 | { 34 | patterns, 35 | }, 36 | ], 37 | }, 38 | ], 39 | invalid: [ 40 | { 41 | code: FULL_TEXT_SECRETS, 42 | options: [ 43 | { 44 | patterns, 45 | }, 46 | ], 47 | errors: Array(5).fill(FULL_TEXT_MATCH_MSG), 48 | }, 49 | ], 50 | }; 51 | } 52 | 53 | RULE_TESTERS.forEach(([version, ruleTester]) => { 54 | ruleTester.run("no-pattern-match", noPatternMatch, createTests(9 <= version)); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/lib/rules/no-secrets.ts: -------------------------------------------------------------------------------- 1 | import P from "../../../src/regexes"; 2 | import { HIGH_ENTROPY, PATTERN_MATCH } from "../../../src/utils"; 3 | import RULE_TESTERS from "./rule-testers"; 4 | import { rules } from "../../../src/index"; 5 | const noSecrets = rules["no-secrets"]; 6 | 7 | const STRING_TEST = ` 8 | const NOT_A_SECRET = "I'm not a secret, I think"; 9 | `; 10 | 11 | const IMPORT_REQUIRE_TEST = ` 12 | const webpackFriendlyConsole = require('./config/webpack/webpackFriendlyConsole') 13 | `; 14 | 15 | const TEMPLATE_TEST = 16 | "const NOT_A_SECRET = `A template that isn't a secret. ${1+1} = 2`"; 17 | 18 | const SECRET_STRING_TEST = ` 19 | const A_SECRET = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; 20 | `; 21 | 22 | const SECRET_LOWERCASE_STRING = ` 23 | const A_LOWERCASE_SECRET = "zwvtjpqsdhwrgl204hc51ycsritmizn8b=/p9uyex7xu6kkagqfm3fj+oobldneva"; 24 | `; 25 | 26 | const A_BEARER_TOKEN = ` 27 | const A_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid%2BULvsea4JtiGRiSDSJSI%3DEUifiRBkKG5E2XzMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F"; 28 | `; 29 | 30 | const IN_AN_OBJECT = ` 31 | const VAULT = { 32 | token:"baaaaaaaaaaaaaaaaaaaamlheaaaaaaa0%2buseid%2bulvsea4jtigrisdsjsi%3deuifirbkkg5e2xzmdjrfl76zc9ub0wnz4xsnirvbchtybjce3f" 33 | } 34 | `; 35 | 36 | const CSS_CLASSNAME = ` 37 | const CSS_CLASSNAME = "hey-it-s-a-css-class-not-a-secret and-neither-this-one"; 38 | `; 39 | const IGNORE_CONTENT_TEST = ` 40 | const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 41 | `; 42 | 43 | const IMPORT_TEST = ` 44 | import {x} from "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; 45 | `; 46 | 47 | const IGNORE_VAR_TEST = `const NOT_A_SECRET = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva";`; 48 | 49 | const IGNORE_FIELD_TEST = ` 50 | const VAULT = { 51 | NOT_A_SECRET:"ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva" 52 | }; 53 | 54 | `; 55 | 56 | const IGNORE_CLASS_FIELD_TEST = ` 57 | class A { 58 | constructor(){ 59 | this.secret = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; 60 | } 61 | } 62 | `; 63 | 64 | const IS_REALLY_A_NAMESPACE_TEST = ` 65 | const NAMESPACE_CLASSNAME = 'Validation.JSONSchemaValidationUtilsImplFactory'; 66 | `; 67 | 68 | const COMMENTS_TEST = ` 69 | // const password = "ZWVTjPQSdhwRgl204Hc51YCsritMIzn8B=/p9UyeX7xu6KkAGqfm3FJ+oObLDNEva"; 70 | const password = ""; 71 | `; 72 | 73 | /** 74 | * Test to make sure regular expressions aren't triggered by the entropy check 75 | */ 76 | const REGEX_TESTS = [ 77 | P["Slack Token"], 78 | P["AWS API Key"], 79 | P["Facebook Oauth"], 80 | P["Twitter Oauth"], 81 | P["Password in URL"], 82 | ].map((regexp) => ({ 83 | code: `const REGEXP = \`${regexp.source}\``, 84 | options: [], 85 | })); 86 | 87 | const HIGH_ENTROPY_MSG = { 88 | messageId: HIGH_ENTROPY, 89 | }; 90 | const PATTERN_MATCH_MSG = { 91 | messageId: PATTERN_MATCH, 92 | }; 93 | 94 | const PATTERN_MATCH_TESTS = [ 95 | P["Google (GCP) Service-account"], 96 | P["RSA private key"], 97 | ].map((regexp) => ({ 98 | code: `const REGEXP = \`${regexp.source}\``, 99 | options: [], 100 | errors: [PATTERN_MATCH_MSG], 101 | })); 102 | 103 | const IMPORT_TEST_LEGACY = { 104 | code: IMPORT_TEST, 105 | options: [{ ignoreModules: true }], 106 | parserOptions: { sourceType: "module", ecmaVersion: 7 }, 107 | }; 108 | 109 | const IMPORT_TEST_FLAT = { 110 | code: IMPORT_TEST, 111 | options: [{ ignoreModules: true }], 112 | languageOptions: { sourceType: "module", ecmaVersion: 7 }, 113 | }; 114 | 115 | export function createTests(flatConfig = false) { 116 | return { 117 | valid: [ 118 | { 119 | code: STRING_TEST, 120 | options: [], 121 | }, 122 | { 123 | code: TEMPLATE_TEST, 124 | options: [], 125 | }, 126 | { 127 | code: IMPORT_REQUIRE_TEST, 128 | options: [], 129 | }, 130 | { 131 | code: CSS_CLASSNAME, 132 | options: [], 133 | }, 134 | { 135 | code: IGNORE_CONTENT_TEST, 136 | options: [{ ignoreContent: [/^ABC/] }], 137 | }, 138 | { 139 | code: IGNORE_CONTENT_TEST, 140 | options: [{ ignoreContent: "^ABC" }], 141 | }, 142 | { 143 | //Property 144 | code: IGNORE_FIELD_TEST, 145 | options: [{ ignoreIdentifiers: [/NOT_A_SECRET/] }], 146 | }, 147 | flatConfig ? IMPORT_TEST_FLAT : IMPORT_TEST_LEGACY, 148 | { 149 | //VariableDeclarator 150 | code: IGNORE_VAR_TEST, 151 | options: [{ ignoreIdentifiers: "NOT_A_SECRET" }], 152 | }, 153 | { 154 | code: IS_REALLY_A_NAMESPACE_TEST, 155 | options: [{ additionalDelimiters: ["."] }], 156 | }, 157 | { 158 | code: IS_REALLY_A_NAMESPACE_TEST, 159 | options: [{ ignoreCase: true }], 160 | }, 161 | ].concat(REGEX_TESTS), 162 | invalid: [ 163 | { 164 | code: SECRET_STRING_TEST, 165 | options: [], 166 | errors: [HIGH_ENTROPY_MSG], 167 | }, 168 | { 169 | code: A_BEARER_TOKEN, 170 | options: [], 171 | errors: [HIGH_ENTROPY_MSG], 172 | }, 173 | { 174 | code: IN_AN_OBJECT, 175 | options: [], 176 | errors: [HIGH_ENTROPY_MSG], 177 | }, 178 | { 179 | code: ` 180 | const BASIC_AUTH_HEADER = "Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l" 181 | `, 182 | options: [ 183 | { 184 | additionalRegexes: { 185 | "Basic Auth": "Authorization: Basic [A-Za-z0-9+/=]*", 186 | }, 187 | }, 188 | ], 189 | errors: [HIGH_ENTROPY_MSG, PATTERN_MATCH_MSG], 190 | }, 191 | { 192 | code: SECRET_LOWERCASE_STRING, 193 | errors: [HIGH_ENTROPY_MSG], 194 | }, 195 | { 196 | code: COMMENTS_TEST, 197 | errors: [HIGH_ENTROPY_MSG], 198 | }, 199 | ].concat(PATTERN_MATCH_TESTS), 200 | }; 201 | } 202 | 203 | RULE_TESTERS.forEach(([version, ruleTester]) => { 204 | ruleTester.run("no-secrets", noSecrets, createTests(9 <= version)); 205 | }); 206 | -------------------------------------------------------------------------------- /tests/lib/rules/rule-testers.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester as RuleTester6 } from "eslint6"; 2 | import { RuleTester as RuleTester7 } from "eslint"; 3 | import { RuleTester as RuleTester9 } from "eslint9"; 4 | import { RuleTester as RuleTester8 } from "eslint8"; 5 | 6 | const RULE_TESTER_CONFIG = { env: { es6: true } }; 7 | 8 | const ruleTester6 = new RuleTester6(RULE_TESTER_CONFIG); 9 | //@ts-ignore 10 | const ruleTester7 = new RuleTester7(RULE_TESTER_CONFIG); 11 | const ruleTester8 = new RuleTester8(RULE_TESTER_CONFIG); 12 | const ruleTester9 = new RuleTester9({ languageOptions: { ecmaVersion: 6 } }); 13 | const RULE_TESTERS = [ 14 | [6, ruleTester6], 15 | [7, ruleTester7], 16 | [8, ruleTester8], 17 | [9, ruleTester9], 18 | ]; 19 | 20 | export default RULE_TESTERS; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | /* Modules */ 7 | "module": "commonjs", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | 11 | "strict": false, 12 | "skipLibCheck": true 13 | }, 14 | "include": [ 15 | "./src/**/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /v1tov2.md: -------------------------------------------------------------------------------- 1 | # v1 to v2 2 | 3 | The breaking changes between v1 to v2 were as follows 4 | 5 | - Removed support for non-supported node & npm versions 6 | - Removed support for ESLint 4 and below 7 | 8 | These versions are no longer supported by their authors, and this being a security module, it means that in all good conscience: I shouldn't be supporting them either. 9 | 10 | That being said, if you find upgrading to newer versions to be unsurmountable, please open an issue. I've been there and I know that feeling, and I'd like to help if I can. 11 | --------------------------------------------------------------------------------