Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move to typescript, start implementing issue #36 #37

Merged
merged 8 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
# Have to use 18.x and up because later versions of eslint require structuredClone
node-version: [18.x, 20.x]
node-version: [18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v4
- name: Run tests against node ${{ matrix.node-version }}
Expand Down
6 changes: 5 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ tests/*
/node_modules/
*.log
staging/*
.travis.yml
.travis.yml
.github/*
src/*
tsconfig.json
v1tov2.md
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# CHANGELOG

## 2.1.0

### Major

- Removed support for non-supported node & npm versions
- Removed support for ESLint 4 and below

### Minor

- Module is now written in typescript

## 1.1.2

### Minor
Expand Down
86 changes: 70 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,31 @@

An eslint rule that searches for potential secrets/keys in code and JSON files.

This plugin has two rules:

- `no-secrets`: Find potential secrets using cryptographic entropy or patterns in the AST (acts like a standard eslint rule, more configurable)
- `no-pattern-match`: Find potential secrets in text (acts like `grep`, less configurable, but potentially more flexible)

---

<!-- vscode-markdown-toc -->

- 1. [Usage](#Usage)
- 1.1. [Flat config](#Flatconfig)
- 1.2. [eslintrc](#eslintrc)
- 1.3. [Include JSON files](#IncludeJSONfiles)
- 1.3.1. [Include JSON files with in "flat configs"](#IncludeJSONfileswithinflatconfigs)
- 2. [Config](#Config)
- 3. [When it's really not a secret](#Whenitsreallynotasecret)
- 3.1. [ Either disable it with a comment](#Eitherdisableitwithacomment)
- 3.2. [ use the `ignoreContent` to ignore certain content](#usetheignoreContenttoignorecertaincontent)
- 3.3. [ Use `ignoreIdentifiers` to ignore certain variable/property names](#UseignoreIdentifierstoignorecertainvariablepropertynames)
- 3.4. [ Use `additionalDelimiters` to further split up tokens](#UseadditionalDelimiterstofurthersplituptokens)
- 4. [Options](#Options)
- 5. [Acknowledgements](#Acknowledgements)
- 2. [`no-secrets`](#no-secrets)
- 2.1. [`no-secrets` examples](#no-secretsexamples)
- 2.2. [When it's really not a secret](#Whenitsreallynotasecret)
- 2.2.1. [ Either disable it with a comment](#Eitherdisableitwithacomment)
- 2.2.2. [ use the `ignoreContent` to ignore certain content](#usetheignoreContenttoignorecertaincontent)
- 2.2.3. [ Use `ignoreIdentifiers` to ignore certain variable/property names](#UseignoreIdentifierstoignorecertainvariablepropertynames)
- 2.2.4. [ Use `additionalDelimiters` to further split up tokens](#UseadditionalDelimiterstofurthersplituptokens)
- 2.3. [`no-secrets` Options](#no-secretsOptions)
- 3. [`no-pattern-match`](#no-pattern-match)
- 3.1. [`no-pattern-match` options](#no-pattern-matchoptions)
- 4. [Acknowledgements](#Acknowledgements)

<!-- vscode-markdown-toc-config
numbering=true
Expand Down Expand Up @@ -107,7 +117,16 @@ export default [
];
```

## 2. <a name='Config'></a>Config
## 2. <a name='no-secrets'></a>`no-secrets`

`no-secrets` is a rule that does two things:

1. Search for patterns that often contain sensitive information
2. Measure cryptographic entropy to find potentially leaked secrets/passwords

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.

### 2.1. <a name='no-secretsexamples'></a>`no-secrets` examples

Decrease the tolerance for entropy

Expand Down Expand Up @@ -139,9 +158,9 @@ Standard patterns can be found [here](./regexes.js)
}
```

## 3. <a name='Whenitsreallynotasecret'></a>When it's really not a secret
### 2.2. <a name='Whenitsreallynotasecret'></a>When it's really not a secret

### 3.1. <a name='Eitherdisableitwithacomment'></a> Either disable it with a comment
#### 2.2.1. <a name='Eitherdisableitwithacomment'></a> Either disable it with a comment

```javascript
// Set of potential base64 characters
Expand All @@ -152,7 +171,7 @@ const BASE64_CHARS =

This will tell future maintainers of the codebase that this suspicious string isn't an oversight

### 3.2. <a name='usetheignoreContenttoignorecertaincontent'></a> use the `ignoreContent` to ignore certain content
#### 2.2.2. <a name='usetheignoreContenttoignorecertaincontent'></a> use the `ignoreContent` to ignore certain content

```json
{
Expand All @@ -163,7 +182,7 @@ This will tell future maintainers of the codebase that this suspicious string is
}
```

### 3.3. <a name='UseignoreIdentifierstoignorecertainvariablepropertynames'></a> Use `ignoreIdentifiers` to ignore certain variable/property names
#### 2.2.3. <a name='UseignoreIdentifierstoignorecertainvariablepropertynames'></a> Use `ignoreIdentifiers` to ignore certain variable/property names

```json
{
Expand All @@ -177,7 +196,7 @@ This will tell future maintainers of the codebase that this suspicious string is
}
```

### 3.4. <a name='UseadditionalDelimiterstofurthersplituptokens'></a> Use `additionalDelimiters` to further split up tokens
#### 2.2.4. <a name='UseadditionalDelimiterstofurthersplituptokens'></a> Use `additionalDelimiters` to further split up tokens

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.

Expand All @@ -195,7 +214,7 @@ For example, if you want to split words up by the character `.` and by camelcase
}
```

## 4. <a name='Options'></a>Options
### 2.3. <a name='no-secretsOptions'></a>`no-secrets` Options

| Option | Description | Default | Type |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------- |
Expand All @@ -207,6 +226,41 @@ For example, if you want to split words up by the character `.` and by camelcase
| ignoreCase | Ignores character case when calculating entropy. This could lead to some false negatives | `false` | `boolean` |
| additionalDelimiters | In addition to splitting the string by whitespace, tokens will be further split by these delimiters | `[]` | (string\|RegExp)[] |

## 5. <a name='Acknowledgements'></a>Acknowledgements
## 3. <a name='no-pattern-match'></a>`no-pattern-match`

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`.

For example, if we configure as follows:

```js
import noSecrets from "eslint-plugin-no-secrets";

//Flat config

export default [
{
files: ["**/*.js"],
plugins: {
"no-secrets": noSecret,
},
rules: {
"no-secrets/no-pattern-match": [
"error",
{ patterns: { SecretJS: /const SECRET/, SecretJSON: /\"SECRET\"/ } },
],
},
},
];
```

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.

### 3.1. <a name='no-pattern-matchoptions'></a>`no-pattern-match` options

| Option | Description | Default | Type |
| -------- | ----------------------------------------------------------------- | ------- | ------------------------------------------- |
| patterns | An object of patterns to check the text contents of files against | `{}` | {\[regexCheckName:string]:string \| RegExp} |

## 4. <a name='Acknowledgements'></a>Acknowledgements

Huge thanks to [truffleHog](https://github.com/dxa4481/truffleHog) for the inspiration, the regexes, and the measure of entropy.
132 changes: 132 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.rules = exports.meta = void 0;
const utils_1 = require("./utils");
const regexes_1 = __importDefault(require("./regexes"));
const no_pattern_match_1 = __importDefault(require("./no-pattern-match"));
function isNonEmptyString(value) {
return !!(value && typeof value === "string");
}
function checkRegexes(value, patterns) {
return Object.keys(patterns)
.map((name) => {
const pattern = patterns[name];
const m = value.match(pattern);
if (!m || !m[0])
return m;
return { name, match: m[0] };
})
.filter((payload) => !!payload);
}
function shouldIgnore(value, toIgnore) {
for (let i = 0; i < toIgnore.length; i++) {
if (value.match(toIgnore[i]))
return true;
}
return false;
}
const meta = {
name: "eslint-plugin-no-secrets",
version: "2.1.0",
};
exports.meta = meta;
const noSecrets = {
meta: {
schema: false,
messages: {
[utils_1.HIGH_ENTROPY]: `Found a string with entropy {{ entropy }} : "{{ token }}"`,
[utils_1.PATTERN_MATCH]: `Found a string that matches "{{ name }}" : "{{ match }}"`,
},
docs: {
description: "An eslint rule that looks for possible leftover secrets in code",
category: "Best Practices",
},
},
create(context) {
const { tolerance, additionalRegexes, ignoreContent, ignoreModules, ignoreIdentifiers, additionalDelimiters, ignoreCase, } = (0, utils_1.checkOptions)(context.options[0] || {});
const sourceCode = context.getSourceCode() || context.sourceCode;
const allPatterns = Object.assign({}, regexes_1.default, additionalRegexes);
const allDelimiters = additionalDelimiters.concat([" "]);
function splitIntoTokens(value) {
let tokens = [value];
allDelimiters.forEach((delimiter) => {
//@ts-ignore
tokens = tokens.map((token) => token.split(delimiter));
//flatten
tokens = [].concat.apply([], tokens);
});
return tokens;
}
function checkEntropy(value) {
value = ignoreCase ? value.toLowerCase() : value;
const tokens = splitIntoTokens(value);
return tokens
.map((token) => {
const entropy = (0, utils_1.shannonEntropy)(token);
return { token, entropy };
})
.filter((payload) => tolerance <= payload.entropy);
}
function entropyReport(data, node) {
//Easier to read numbers
data.entropy = Math.round(data.entropy * 100) / 100;
context.report({
node,
data,
messageId: utils_1.HIGH_ENTROPY,
});
}
function patternReport(data, node) {
context.report({
node,
data,
messageId: utils_1.PATTERN_MATCH,
});
}
function checkString(value, node) {
const idName = (0, utils_1.getIdentifierName)(node);
if (idName && shouldIgnore(idName, ignoreIdentifiers))
return;
if (!isNonEmptyString(value))
return;
if (ignoreModules && (0, utils_1.isModulePathString)(node)) {
return;
}
if (shouldIgnore(value, ignoreContent))
return;
checkEntropy(value).forEach((payload) => {
entropyReport(payload, node);
});
checkRegexes(value, allPatterns).forEach((payload) => {
patternReport(payload, node);
});
}
//Check all comments
const comments = sourceCode.getAllComments();
comments.forEach((comment) => checkString(comment.value, comment));
return {
Literal(node) {
const { value } = node;
checkString(value, node);
},
TemplateElement(node) {
if (!node.value)
return;
const value = node.value.cooked;
checkString(value, node);
},
JSONLiteral(node) {
const { value } = node;
checkString(value, node);
},
};
},
};
const rules = {
"no-pattern-match": no_pattern_match_1.default,
"no-secrets": noSecrets,
};
exports.rules = rules;
Loading