├── .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 | [](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 |
--------------------------------------------------------------------------------