eslint-plugin-anthropic-security 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +66 -0
- package/dist/package.json +95 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +39 -0
- package/dist/src/oxlint.d.ts +20 -0
- package/dist/src/oxlint.js +21 -0
- package/dist/src/rules/no-hardcoded-api-key/index.d.ts +8 -0
- package/dist/src/rules/no-hardcoded-api-key/index.js +113 -0
- package/package.json +95 -0
- package/src/index.test.ts +32 -0
- package/src/index.ts +53 -0
- package/src/oxlint.ts +23 -0
- package/src/rules/no-hardcoded-api-key/index.ts +121 -0
- package/src/rules/no-hardcoded-api-key/no-hardcoded-api-key.test.ts +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ofri Peretz
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-anthropic-security" target="blank"><img src="https://eslint.interlace.tools/icon-light.svg" alt="Interlace" height="90" /></a>
|
|
3
|
+
|
|
4
|
+
<a href="https://eslint.org" target="_blank"><img src="https://eslint.interlace.tools/eslint-logo.svg" alt="ESLint" height="90" /></a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Security rules for the Anthropic SDK and Claude Agent SDK.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/eslint-plugin-anthropic-security" target="_blank"><img src="https://img.shields.io/npm/v/eslint-plugin-anthropic-security.svg" alt="NPM Version" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/eslint-plugin-anthropic-security" target="_blank"><img src="https://img.shields.io/npm/dm/eslint-plugin-anthropic-security.svg" alt="NPM Downloads" /></a>
|
|
14
|
+
<a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="Package License" /></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
> ⭐ If this plugin caught a real bug for you, [**star the repo**](https://github.com/ofri-peretz/eslint) — it's the signal that keeps these rules maintained.
|
|
18
|
+
|
|
19
|
+
## Description
|
|
20
|
+
|
|
21
|
+
Security rules for [`@anthropic-ai/sdk`](https://www.npmjs.com/package/@anthropic-ai/sdk).
|
|
22
|
+
|
|
23
|
+
Every rule gates on the SDK actually being imported, so the plugin stays silent in files that don't use it.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install --save-dev eslint-plugin-anthropic-security
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
// eslint.config.js
|
|
35
|
+
import anthropicSecurity from 'eslint-plugin-anthropic-security';
|
|
36
|
+
|
|
37
|
+
export default [
|
|
38
|
+
anthropicSecurity.configs.recommended,
|
|
39
|
+
];
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### oxlint
|
|
43
|
+
|
|
44
|
+
Every rule runs on [oxlint](https://oxc.rs) as well as ESLint:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{ "jsPlugins": ["eslint-plugin-anthropic-security/oxlint"] }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Rules
|
|
51
|
+
|
|
52
|
+
| Rule | Description | CWE | Recommended |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| [no-hardcoded-api-key](./docs/rules/no-hardcoded-api-key.md) | Forbid a literal API key in the Anthropic client options | CWE-798 | ✅ error |
|
|
55
|
+
|
|
56
|
+
## 📄 License
|
|
57
|
+
|
|
58
|
+
MIT © [Ofri Peretz](https://github.com/ofri-peretz)
|
|
59
|
+
|
|
60
|
+
<p align="center">
|
|
61
|
+
<a href="https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-anthropic-security" target="blank"><img src="https://eslint.interlace.tools/icon-light.svg" alt="Interlace" height="70" /></a>
|
|
62
|
+
</p>
|
package/dist/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ofri Peretz
|
|
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.
|
package/dist/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-anthropic-security" target="blank"><img src="https://eslint.interlace.tools/icon-light.svg" alt="Interlace" height="90" /></a>
|
|
3
|
+
|
|
4
|
+
<a href="https://eslint.org" target="_blank"><img src="https://eslint.interlace.tools/eslint-logo.svg" alt="ESLint" height="90" /></a>
|
|
5
|
+
|
|
6
|
+
<a href="https://oxc.rs" target="_blank"><img src="https://eslint.interlace.tools/oxlint-logo.svg" alt="oxlint" height="90" /></a>
|
|
7
|
+
|
|
8
|
+
<a href="https://www.npmjs.com/package/@anthropic-ai/sdk" target="_blank"><img src="https://eslint.interlace.tools/anthropic-logo.svg" alt="Anthropic" height="90" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
Security rules for the Anthropic SDK and Claude Agent SDK.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://www.npmjs.com/package/eslint-plugin-anthropic-security" target="_blank"><img src="https://img.shields.io/npm/v/eslint-plugin-anthropic-security.svg" alt="NPM Version" /></a>
|
|
17
|
+
<a href="https://www.npmjs.com/package/eslint-plugin-anthropic-security" target="_blank"><img src="https://img.shields.io/npm/dm/eslint-plugin-anthropic-security.svg" alt="NPM Downloads" /></a>
|
|
18
|
+
<a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="Package License" /></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
> ⭐ If this plugin caught a real bug for you, [**star the repo**](https://github.com/ofri-peretz/eslint) — it's the signal that keeps these rules maintained.
|
|
22
|
+
|
|
23
|
+
## Description
|
|
24
|
+
|
|
25
|
+
Security rules for [`@anthropic-ai/sdk`](https://www.npmjs.com/package/@anthropic-ai/sdk).
|
|
26
|
+
|
|
27
|
+
Every rule gates on the SDK actually being imported, so the plugin stays silent in files that don't use it.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install --save-dev eslint-plugin-anthropic-security
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// eslint.config.js
|
|
39
|
+
import anthropicSecurity from 'eslint-plugin-anthropic-security';
|
|
40
|
+
|
|
41
|
+
export default [
|
|
42
|
+
anthropicSecurity.configs.recommended,
|
|
43
|
+
];
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### oxlint
|
|
47
|
+
|
|
48
|
+
Every rule runs on [oxlint](https://oxc.rs) as well as ESLint:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{ "jsPlugins": ["eslint-plugin-anthropic-security/oxlint"] }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
|
|
56
|
+
| Rule | Description | CWE | Recommended |
|
|
57
|
+
| --- | --- | --- | --- |
|
|
58
|
+
| [no-hardcoded-api-key](./docs/rules/no-hardcoded-api-key.md) | Forbid a literal API key in the Anthropic client options | CWE-798 | ✅ error |
|
|
59
|
+
|
|
60
|
+
## 📄 License
|
|
61
|
+
|
|
62
|
+
MIT © [Ofri Peretz](https://github.com/ofri-peretz)
|
|
63
|
+
|
|
64
|
+
<p align="center">
|
|
65
|
+
<a href="https://eslint.interlace.tools/?utm_source=github&utm_medium=referral&utm_campaign=eslint-plugin-anthropic-security" target="blank"><img src="https://eslint.interlace.tools/icon-light.svg" alt="Interlace" height="70" /></a>
|
|
66
|
+
</p>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-anthropic-security",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint plugin for Anthropic SDK security — catches hardcoded API keys in the Claude client options.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./oxlint": {
|
|
14
|
+
"types": "./src/oxlint.d.ts",
|
|
15
|
+
"default": "./src/oxlint.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"author": "Ofri Peretz <ofriperetzdev@gmail.com>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://eslint.interlace.tools/docs/security/plugin-anthropic-security?utm_source=npm&utm_medium=referral&utm_campaign=eslint-plugin-anthropic-security",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/ofri-peretz/eslint",
|
|
24
|
+
"directory": "packages/eslint-plugin-anthropic-security"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/ofri-peretz/eslint/issues"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"src/",
|
|
34
|
+
"dist/",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"CHANGELOG.md"
|
|
38
|
+
],
|
|
39
|
+
"keywords": [
|
|
40
|
+
"eslint",
|
|
41
|
+
"eslintplugin",
|
|
42
|
+
"eslint-plugin",
|
|
43
|
+
"static-analysis",
|
|
44
|
+
"linting",
|
|
45
|
+
"code-quality",
|
|
46
|
+
"security",
|
|
47
|
+
"sast",
|
|
48
|
+
"appsec",
|
|
49
|
+
"vulnerability",
|
|
50
|
+
"owasp",
|
|
51
|
+
"cwe",
|
|
52
|
+
"interlace-security",
|
|
53
|
+
"ai-security",
|
|
54
|
+
"llm",
|
|
55
|
+
"llm-security",
|
|
56
|
+
"typescript",
|
|
57
|
+
"anthropic",
|
|
58
|
+
"claude",
|
|
59
|
+
"anthropic-sdk",
|
|
60
|
+
"claude-agent-sdk",
|
|
61
|
+
"agents"
|
|
62
|
+
],
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18.0.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@anthropic-ai/sdk": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@anthropic-ai/claude-agent-sdk": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"tslib": "^2.3.0",
|
|
79
|
+
"@interlace/eslint-devkit": "^1.4.4"
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@typescript-eslint/parser": "^8.46.2",
|
|
83
|
+
"@typescript-eslint/rule-tester": "^8.46.2"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {
|
|
86
|
+
"build": "tsx ../../scripts/build-package.ts",
|
|
87
|
+
"test": "vitest run",
|
|
88
|
+
"test:coverage": "vitest run --coverage",
|
|
89
|
+
"typecheck": "tsc -p tsconfig.lib.json --noEmit"
|
|
90
|
+
},
|
|
91
|
+
"funding": {
|
|
92
|
+
"type": "github",
|
|
93
|
+
"url": "https://github.com/ofri-peretz/eslint"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* eslint-plugin-anthropic-security
|
|
8
|
+
*
|
|
9
|
+
* Security rules for the Anthropic SDK and Claude Agent SDK.
|
|
10
|
+
*
|
|
11
|
+
* Scope promise: every rule gates on the SDK being imported, so the plugin
|
|
12
|
+
* stays silent in files that do not use it.
|
|
13
|
+
*/
|
|
14
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
15
|
+
export { noHardcodedApiKey } from './rules/no-hardcoded-api-key';
|
|
16
|
+
export declare const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>>;
|
|
17
|
+
export declare const plugin: TSESLint.FlatConfig.Plugin;
|
|
18
|
+
export declare const configs: {
|
|
19
|
+
minimal: TSESLint.FlatConfig.Config;
|
|
20
|
+
recommended: TSESLint.FlatConfig.Config;
|
|
21
|
+
strict: TSESLint.FlatConfig.Config;
|
|
22
|
+
};
|
|
23
|
+
declare const _default: {
|
|
24
|
+
configs: {
|
|
25
|
+
minimal: TSESLint.FlatConfig.Config;
|
|
26
|
+
recommended: TSESLint.FlatConfig.Config;
|
|
27
|
+
strict: TSESLint.FlatConfig.Config;
|
|
28
|
+
};
|
|
29
|
+
meta?: { [K in keyof TSESLint.FlatConfig.PluginMeta]?: TSESLint.FlatConfig.PluginMeta[K] | undefined; };
|
|
30
|
+
processors?: Partial<Record<string, TSESLint.FlatConfig.Processor>> | undefined;
|
|
31
|
+
rules?: Record<string, TSESLint.LooseRuleDefinition> | undefined;
|
|
32
|
+
};
|
|
33
|
+
export default _default;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.configs = exports.plugin = exports.rules = exports.noHardcodedApiKey = void 0;
|
|
9
|
+
const no_hardcoded_api_key_1 = require("./rules/no-hardcoded-api-key");
|
|
10
|
+
var no_hardcoded_api_key_2 = require("./rules/no-hardcoded-api-key");
|
|
11
|
+
Object.defineProperty(exports, "noHardcodedApiKey", { enumerable: true, get: function () { return no_hardcoded_api_key_2.noHardcodedApiKey; } });
|
|
12
|
+
exports.rules = {
|
|
13
|
+
// CWE-798
|
|
14
|
+
'no-hardcoded-api-key': no_hardcoded_api_key_1.noHardcodedApiKey,
|
|
15
|
+
};
|
|
16
|
+
exports.plugin = {
|
|
17
|
+
meta: {
|
|
18
|
+
name: 'eslint-plugin-anthropic-security',
|
|
19
|
+
version: '0.1.0',
|
|
20
|
+
},
|
|
21
|
+
rules: exports.rules,
|
|
22
|
+
};
|
|
23
|
+
const enabled = {
|
|
24
|
+
plugins: {
|
|
25
|
+
'anthropic-security': exports.plugin,
|
|
26
|
+
},
|
|
27
|
+
rules: {
|
|
28
|
+
'anthropic-security/no-hardcoded-api-key': 'error',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
exports.configs = {
|
|
32
|
+
minimal: enabled,
|
|
33
|
+
recommended: enabled,
|
|
34
|
+
strict: enabled,
|
|
35
|
+
};
|
|
36
|
+
exports.default = {
|
|
37
|
+
...exports.plugin,
|
|
38
|
+
configs: exports.configs,
|
|
39
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* oxlint sub-export.
|
|
8
|
+
*
|
|
9
|
+
* Consumers wire oxlint to this entry via:
|
|
10
|
+
* { "jsPlugins": ["eslint-plugin-anthropic-security/oxlint"] }
|
|
11
|
+
*
|
|
12
|
+
* oxlint's JS plugin loader does `require('eslint-plugin-anthropic-security/oxlint')` and reads
|
|
13
|
+
* `.rules` off the result. The compiled output of `export = plugin` is
|
|
14
|
+
* `module.exports = plugin`, which gives oxlint the plugin object directly
|
|
15
|
+
* (`{ meta, rules }`) — no wrapper, no `.default` indirection.
|
|
16
|
+
*
|
|
17
|
+
* Generated by scripts/generate-oxlint-shims.mjs. Do not edit by hand.
|
|
18
|
+
*/
|
|
19
|
+
import { plugin } from './index';
|
|
20
|
+
export = plugin;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* oxlint sub-export.
|
|
9
|
+
*
|
|
10
|
+
* Consumers wire oxlint to this entry via:
|
|
11
|
+
* { "jsPlugins": ["eslint-plugin-anthropic-security/oxlint"] }
|
|
12
|
+
*
|
|
13
|
+
* oxlint's JS plugin loader does `require('eslint-plugin-anthropic-security/oxlint')` and reads
|
|
14
|
+
* `.rules` off the result. The compiled output of `export = plugin` is
|
|
15
|
+
* `module.exports = plugin`, which gives oxlint the plugin object directly
|
|
16
|
+
* (`{ meta, rules }`) — no wrapper, no `.default` indirection.
|
|
17
|
+
*
|
|
18
|
+
* Generated by scripts/generate-oxlint-shims.mjs. Do not edit by hand.
|
|
19
|
+
*/
|
|
20
|
+
const index_1 = require("./index");
|
|
21
|
+
module.exports = index_1.plugin;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
export declare const noHardcodedApiKey: import("@typescript-eslint/utils/ts-eslint").RuleModule<"hardcodedApiKey", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noHardcodedApiKey = void 0;
|
|
9
|
+
/**
|
|
10
|
+
* @fileoverview Forbid a literal API key in the Anthropic client options
|
|
11
|
+
* @description A key written into source is committed, pushed, mirrored into
|
|
12
|
+
* every clone and CI cache, and is billable by anyone who reads it. Rotating it
|
|
13
|
+
* means a code change, so leaked keys tend to stay live.
|
|
14
|
+
* @see https://docs.anthropic.com/en/api/getting-started
|
|
15
|
+
*/
|
|
16
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
17
|
+
/** Covers the base SDK and the agent SDK, which share the client options. */
|
|
18
|
+
const ANTHROPIC_MODULE_PREFIX = '@anthropic-ai/';
|
|
19
|
+
const KEY_PROPS = new Set(['apiKey', 'authToken']);
|
|
20
|
+
/**
|
|
21
|
+
* Whether the client options literal carries an inline credential.
|
|
22
|
+
*
|
|
23
|
+
* `process.env.ANTHROPIC_API_KEY` and any other non-literal expression are
|
|
24
|
+
* `safe` — reading a key from the environment is the correct pattern. A spread
|
|
25
|
+
* is `unreadable`: the key may be in the spread source, and guessing there
|
|
26
|
+
* would flag correct code.
|
|
27
|
+
*/
|
|
28
|
+
function readCredential(options) {
|
|
29
|
+
for (const prop of options.properties) {
|
|
30
|
+
if (prop.type === 'SpreadElement')
|
|
31
|
+
return 'unreadable';
|
|
32
|
+
if (prop.computed)
|
|
33
|
+
continue;
|
|
34
|
+
const isCredential = (prop.key.type === 'Identifier' && KEY_PROPS.has(prop.key.name)) ||
|
|
35
|
+
(prop.key.type === 'Literal' && KEY_PROPS.has(String(prop.key.value)));
|
|
36
|
+
if (!isCredential)
|
|
37
|
+
continue;
|
|
38
|
+
if (prop.value.type !== 'Literal')
|
|
39
|
+
return 'safe';
|
|
40
|
+
// An empty string is a placeholder, not a credential.
|
|
41
|
+
if (typeof prop.value.value === 'string' && prop.value.value.length > 0)
|
|
42
|
+
return 'literal';
|
|
43
|
+
return 'safe';
|
|
44
|
+
}
|
|
45
|
+
return 'safe';
|
|
46
|
+
}
|
|
47
|
+
exports.noHardcodedApiKey = (0, eslint_devkit_1.createRule)({
|
|
48
|
+
name: 'no-hardcoded-api-key',
|
|
49
|
+
meta: {
|
|
50
|
+
type: 'problem',
|
|
51
|
+
docs: {
|
|
52
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-anthropic-security/docs/rules/no-hardcoded-api-key.md',
|
|
53
|
+
description: 'Forbid a literal API key in the Anthropic client options',
|
|
54
|
+
cwe: 'CWE-798',
|
|
55
|
+
cvss: 9.1,
|
|
56
|
+
},
|
|
57
|
+
messages: {
|
|
58
|
+
hardcodedApiKey: (0, eslint_devkit_1.formatLLMMessage)({
|
|
59
|
+
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
60
|
+
issueName: 'Hardcoded Anthropic Credential',
|
|
61
|
+
cwe: 'CWE-798',
|
|
62
|
+
owasp: 'A07:2021',
|
|
63
|
+
cvss: 9.1,
|
|
64
|
+
description: '{{prop}} is a string literal, so the credential is committed to source control and readable by anyone with repository access',
|
|
65
|
+
severity: 'CRITICAL',
|
|
66
|
+
compliance: ['SOC2', 'PCI-DSS'],
|
|
67
|
+
fix: 'Read it from the environment: new Anthropic({ {{prop}}: process.env.ANTHROPIC_API_KEY })',
|
|
68
|
+
documentationLink: 'https://docs.anthropic.com/en/api/getting-started',
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
schema: [],
|
|
72
|
+
},
|
|
73
|
+
defaultOptions: [],
|
|
74
|
+
create(context) {
|
|
75
|
+
let importsAnthropic = false;
|
|
76
|
+
const candidates = [];
|
|
77
|
+
function inspect(node, args) {
|
|
78
|
+
const options = args[0];
|
|
79
|
+
if (options?.type !== 'ObjectExpression')
|
|
80
|
+
return;
|
|
81
|
+
if (readCredential(options) === 'literal') {
|
|
82
|
+
candidates.push({ node, prop: 'apiKey' });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
ImportDeclaration(node) {
|
|
87
|
+
if (node.source.value.startsWith(ANTHROPIC_MODULE_PREFIX))
|
|
88
|
+
importsAnthropic = true;
|
|
89
|
+
},
|
|
90
|
+
NewExpression(node) {
|
|
91
|
+
inspect(node, [...node.arguments]);
|
|
92
|
+
},
|
|
93
|
+
CallExpression(node) {
|
|
94
|
+
if (node.callee.type === 'Identifier' &&
|
|
95
|
+
node.callee.name === 'require' &&
|
|
96
|
+
node.arguments[0]?.type === 'Literal' &&
|
|
97
|
+
typeof node.arguments[0].value === 'string' &&
|
|
98
|
+
node.arguments[0].value.startsWith(ANTHROPIC_MODULE_PREFIX)) {
|
|
99
|
+
importsAnthropic = true;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
inspect(node, [...node.arguments]);
|
|
103
|
+
},
|
|
104
|
+
'Program:exit'() {
|
|
105
|
+
if (!importsAnthropic)
|
|
106
|
+
return;
|
|
107
|
+
for (const { node, prop } of candidates) {
|
|
108
|
+
context.report({ node, messageId: 'hardcodedApiKey', data: { prop } });
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-anthropic-security",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint plugin for Anthropic SDK security \u2014 catches hardcoded API keys in the Claude client options.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./dist/src/index.js",
|
|
7
|
+
"types": "./dist/src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/src/index.d.ts",
|
|
11
|
+
"default": "./dist/src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./oxlint": {
|
|
14
|
+
"types": "./src/oxlint.d.ts",
|
|
15
|
+
"default": "./src/oxlint.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"author": "Ofri Peretz <ofriperetzdev@gmail.com>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://eslint.interlace.tools/docs/security/plugin-anthropic-security?utm_source=npm&utm_medium=referral&utm_campaign=eslint-plugin-anthropic-security",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/ofri-peretz/eslint",
|
|
24
|
+
"directory": "packages/eslint-plugin-anthropic-security"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/ofri-peretz/eslint/issues"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"src/",
|
|
34
|
+
"dist/",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"CHANGELOG.md"
|
|
38
|
+
],
|
|
39
|
+
"keywords": [
|
|
40
|
+
"eslint",
|
|
41
|
+
"eslintplugin",
|
|
42
|
+
"eslint-plugin",
|
|
43
|
+
"static-analysis",
|
|
44
|
+
"linting",
|
|
45
|
+
"code-quality",
|
|
46
|
+
"security",
|
|
47
|
+
"sast",
|
|
48
|
+
"appsec",
|
|
49
|
+
"vulnerability",
|
|
50
|
+
"owasp",
|
|
51
|
+
"cwe",
|
|
52
|
+
"interlace-security",
|
|
53
|
+
"ai-security",
|
|
54
|
+
"llm",
|
|
55
|
+
"llm-security",
|
|
56
|
+
"typescript",
|
|
57
|
+
"anthropic",
|
|
58
|
+
"claude",
|
|
59
|
+
"anthropic-sdk",
|
|
60
|
+
"claude-agent-sdk",
|
|
61
|
+
"agents"
|
|
62
|
+
],
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18.0.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@anthropic-ai/sdk": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@anthropic-ai/claude-agent-sdk": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"tslib": "^2.3.0",
|
|
79
|
+
"@interlace/eslint-devkit": "^1.4.4"
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@typescript-eslint/parser": "^8.46.2",
|
|
83
|
+
"@typescript-eslint/rule-tester": "^8.46.2"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {
|
|
86
|
+
"build": "tsx ../../scripts/build-package.ts",
|
|
87
|
+
"test": "vitest run",
|
|
88
|
+
"test:coverage": "vitest run --coverage",
|
|
89
|
+
"typecheck": "tsc -p tsconfig.lib.json --noEmit"
|
|
90
|
+
},
|
|
91
|
+
"funding": {
|
|
92
|
+
"type": "github",
|
|
93
|
+
"url": "https://github.com/ofri-peretz/eslint"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Plugin surface lock for eslint-plugin-anthropic-security.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import plugin, { rules, configs, plugin as namedPlugin, noHardcodedApiKey } from './index';
|
|
8
|
+
|
|
9
|
+
describe('eslint-plugin-anthropic-security', () => {
|
|
10
|
+
it('exposes every rule under its documented id', () => {
|
|
11
|
+
expect(Object.keys(rules).sort()).toEqual(['no-hardcoded-api-key']);
|
|
12
|
+
expect(rules['no-hardcoded-api-key']).toBe(noHardcodedApiKey);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('names itself for the oxlint loader', () => {
|
|
16
|
+
expect(namedPlugin.meta?.name).toBe('eslint-plugin-anthropic-security');
|
|
17
|
+
expect(namedPlugin.rules).toBe(rules);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('ships minimal, recommended and strict configs', () => {
|
|
21
|
+
expect(Object.keys(configs).sort()).toEqual(['minimal', 'recommended', 'strict']);
|
|
22
|
+
for (const config of Object.values(configs)) {
|
|
23
|
+
expect(config.plugins).toHaveProperty('anthropic-security');
|
|
24
|
+
expect(config.rules?.['anthropic-security/no-hardcoded-api-key']).toBe('error');
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('default-exports the plugin with its configs attached', () => {
|
|
29
|
+
expect(plugin.rules).toBe(rules);
|
|
30
|
+
expect(plugin.configs).toBe(configs);
|
|
31
|
+
});
|
|
32
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* eslint-plugin-anthropic-security
|
|
9
|
+
*
|
|
10
|
+
* Security rules for the Anthropic SDK and Claude Agent SDK.
|
|
11
|
+
*
|
|
12
|
+
* Scope promise: every rule gates on the SDK being imported, so the plugin
|
|
13
|
+
* stays silent in files that do not use it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
17
|
+
|
|
18
|
+
import { noHardcodedApiKey } from './rules/no-hardcoded-api-key';
|
|
19
|
+
|
|
20
|
+
export { noHardcodedApiKey } from './rules/no-hardcoded-api-key';
|
|
21
|
+
|
|
22
|
+
export const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
23
|
+
// CWE-798
|
|
24
|
+
'no-hardcoded-api-key': noHardcodedApiKey,
|
|
25
|
+
} satisfies Record<string, TSESLint.RuleModule<string, readonly unknown[]>>;
|
|
26
|
+
|
|
27
|
+
export const plugin: TSESLint.FlatConfig.Plugin = {
|
|
28
|
+
meta: {
|
|
29
|
+
name: 'eslint-plugin-anthropic-security',
|
|
30
|
+
version: '0.1.0',
|
|
31
|
+
},
|
|
32
|
+
rules,
|
|
33
|
+
} satisfies TSESLint.FlatConfig.Plugin;
|
|
34
|
+
|
|
35
|
+
const enabled: TSESLint.FlatConfig.Config = {
|
|
36
|
+
plugins: {
|
|
37
|
+
'anthropic-security': plugin,
|
|
38
|
+
},
|
|
39
|
+
rules: {
|
|
40
|
+
'anthropic-security/no-hardcoded-api-key': 'error',
|
|
41
|
+
},
|
|
42
|
+
} satisfies TSESLint.FlatConfig.Config;
|
|
43
|
+
|
|
44
|
+
export const configs = {
|
|
45
|
+
minimal: enabled,
|
|
46
|
+
recommended: enabled,
|
|
47
|
+
strict: enabled,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default {
|
|
51
|
+
...plugin,
|
|
52
|
+
configs,
|
|
53
|
+
};
|
package/src/oxlint.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* oxlint sub-export.
|
|
9
|
+
*
|
|
10
|
+
* Consumers wire oxlint to this entry via:
|
|
11
|
+
* { "jsPlugins": ["eslint-plugin-anthropic-security/oxlint"] }
|
|
12
|
+
*
|
|
13
|
+
* oxlint's JS plugin loader does `require('eslint-plugin-anthropic-security/oxlint')` and reads
|
|
14
|
+
* `.rules` off the result. The compiled output of `export = plugin` is
|
|
15
|
+
* `module.exports = plugin`, which gives oxlint the plugin object directly
|
|
16
|
+
* (`{ meta, rules }`) — no wrapper, no `.default` indirection.
|
|
17
|
+
*
|
|
18
|
+
* Generated by scripts/generate-oxlint-shims.mjs. Do not edit by hand.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { plugin } from './index';
|
|
22
|
+
|
|
23
|
+
export = plugin;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview Forbid a literal API key in the Anthropic client options
|
|
9
|
+
* @description A key written into source is committed, pushed, mirrored into
|
|
10
|
+
* every clone and CI cache, and is billable by anyone who reads it. Rotating it
|
|
11
|
+
* means a code change, so leaked keys tend to stay live.
|
|
12
|
+
* @see https://docs.anthropic.com/en/api/getting-started
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { TSESTree, createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
|
|
16
|
+
|
|
17
|
+
type MessageIds = 'hardcodedApiKey';
|
|
18
|
+
|
|
19
|
+
/** Covers the base SDK and the agent SDK, which share the client options. */
|
|
20
|
+
const ANTHROPIC_MODULE_PREFIX = '@anthropic-ai/';
|
|
21
|
+
|
|
22
|
+
const KEY_PROPS = new Set(['apiKey', 'authToken']);
|
|
23
|
+
|
|
24
|
+
type KeyVerdict = 'literal' | 'safe' | 'unreadable';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether the client options literal carries an inline credential.
|
|
28
|
+
*
|
|
29
|
+
* `process.env.ANTHROPIC_API_KEY` and any other non-literal expression are
|
|
30
|
+
* `safe` — reading a key from the environment is the correct pattern. A spread
|
|
31
|
+
* is `unreadable`: the key may be in the spread source, and guessing there
|
|
32
|
+
* would flag correct code.
|
|
33
|
+
*/
|
|
34
|
+
function readCredential(options: TSESTree.ObjectExpression): KeyVerdict {
|
|
35
|
+
for (const prop of options.properties) {
|
|
36
|
+
if (prop.type === 'SpreadElement') return 'unreadable';
|
|
37
|
+
if (prop.computed) continue;
|
|
38
|
+
const isCredential =
|
|
39
|
+
(prop.key.type === 'Identifier' && KEY_PROPS.has(prop.key.name)) ||
|
|
40
|
+
(prop.key.type === 'Literal' && KEY_PROPS.has(String(prop.key.value)));
|
|
41
|
+
if (!isCredential) continue;
|
|
42
|
+
if (prop.value.type !== 'Literal') return 'safe';
|
|
43
|
+
// An empty string is a placeholder, not a credential.
|
|
44
|
+
if (typeof prop.value.value === 'string' && prop.value.value.length > 0) return 'literal';
|
|
45
|
+
return 'safe';
|
|
46
|
+
}
|
|
47
|
+
return 'safe';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const noHardcodedApiKey = createRule<[], MessageIds>({
|
|
51
|
+
name: 'no-hardcoded-api-key',
|
|
52
|
+
meta: {
|
|
53
|
+
type: 'problem',
|
|
54
|
+
docs: {
|
|
55
|
+
url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-anthropic-security/docs/rules/no-hardcoded-api-key.md',
|
|
56
|
+
description: 'Forbid a literal API key in the Anthropic client options',
|
|
57
|
+
cwe: 'CWE-798',
|
|
58
|
+
cvss: 9.1,
|
|
59
|
+
},
|
|
60
|
+
messages: {
|
|
61
|
+
hardcodedApiKey: formatLLMMessage({
|
|
62
|
+
icon: MessageIcons.SECURITY,
|
|
63
|
+
issueName: 'Hardcoded Anthropic Credential',
|
|
64
|
+
cwe: 'CWE-798',
|
|
65
|
+
owasp: 'A07:2021',
|
|
66
|
+
cvss: 9.1,
|
|
67
|
+
description:
|
|
68
|
+
'{{prop}} is a string literal, so the credential is committed to source control and readable by anyone with repository access',
|
|
69
|
+
severity: 'CRITICAL',
|
|
70
|
+
compliance: ['SOC2', 'PCI-DSS'],
|
|
71
|
+
fix: 'Read it from the environment: new Anthropic({ {{prop}}: process.env.ANTHROPIC_API_KEY })',
|
|
72
|
+
documentationLink: 'https://docs.anthropic.com/en/api/getting-started',
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
schema: [],
|
|
76
|
+
},
|
|
77
|
+
defaultOptions: [],
|
|
78
|
+
create(context) {
|
|
79
|
+
let importsAnthropic = false;
|
|
80
|
+
const candidates: Array<{ node: TSESTree.Node; prop: string }> = [];
|
|
81
|
+
|
|
82
|
+
function inspect(node: TSESTree.Node, args: TSESTree.CallExpressionArgument[]): void {
|
|
83
|
+
const options = args[0];
|
|
84
|
+
if (options?.type !== 'ObjectExpression') return;
|
|
85
|
+
if (readCredential(options) === 'literal') {
|
|
86
|
+
candidates.push({ node, prop: 'apiKey' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
ImportDeclaration(node: TSESTree.ImportDeclaration) {
|
|
92
|
+
if (node.source.value.startsWith(ANTHROPIC_MODULE_PREFIX)) importsAnthropic = true;
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
NewExpression(node: TSESTree.NewExpression) {
|
|
96
|
+
inspect(node, [...node.arguments]);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
CallExpression(node: TSESTree.CallExpression) {
|
|
100
|
+
if (
|
|
101
|
+
node.callee.type === 'Identifier' &&
|
|
102
|
+
node.callee.name === 'require' &&
|
|
103
|
+
node.arguments[0]?.type === 'Literal' &&
|
|
104
|
+
typeof node.arguments[0].value === 'string' &&
|
|
105
|
+
node.arguments[0].value.startsWith(ANTHROPIC_MODULE_PREFIX)
|
|
106
|
+
) {
|
|
107
|
+
importsAnthropic = true;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
inspect(node, [...node.arguments]);
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
'Program:exit'() {
|
|
114
|
+
if (!importsAnthropic) return;
|
|
115
|
+
for (const { node, prop } of candidates) {
|
|
116
|
+
context.report({ node, messageId: 'hardcodedApiKey', data: { prop } });
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for no-hardcoded-api-key (Anthropic)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
6
|
+
import { noHardcodedApiKey } from './index';
|
|
7
|
+
|
|
8
|
+
const ruleTester = new RuleTester({
|
|
9
|
+
languageOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const IMPORT = `import Anthropic from '@anthropic-ai/sdk';`;
|
|
13
|
+
|
|
14
|
+
ruleTester.run('no-hardcoded-api-key', noHardcodedApiKey, {
|
|
15
|
+
valid: [
|
|
16
|
+
{ code: `${IMPORT}\nconst c = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });` },
|
|
17
|
+
// Empty string is a placeholder, not a credential
|
|
18
|
+
{ code: `${IMPORT}\nconst c = new Anthropic({ apiKey: '' });` },
|
|
19
|
+
// Non-string literal
|
|
20
|
+
{ code: `${IMPORT}\nconst c = new Anthropic({ apiKey: null });` },
|
|
21
|
+
// Spread could carry the key
|
|
22
|
+
{ code: `${IMPORT}\nconst c = new Anthropic({ ...opts });` },
|
|
23
|
+
// Computed key
|
|
24
|
+
{ code: `${IMPORT}\nconst c = new Anthropic({ [k]: 'sk-ant-xxx' });` },
|
|
25
|
+
// Unrelated property holding a literal
|
|
26
|
+
{ code: `${IMPORT}\nconst c = new Anthropic({ baseURL: 'https://api.anthropic.com' });` },
|
|
27
|
+
// No options / non-object options
|
|
28
|
+
{ code: `${IMPORT}\nconst c = new Anthropic();` },
|
|
29
|
+
{ code: `${IMPORT}\nconst c = new Anthropic(opts);` },
|
|
30
|
+
// Scope promise: no Anthropic import, no finding
|
|
31
|
+
{ code: `const c = new Anthropic({ apiKey: 'sk-ant-hardcoded' });` },
|
|
32
|
+
// require of an unrelated module
|
|
33
|
+
{ code: `const z = require('zod');\nnew Anthropic({ apiKey: 'sk-ant-hardcoded' });` },
|
|
34
|
+
// require with a non-string argument
|
|
35
|
+
{ code: `const m = require(dyn);\nnew Anthropic({ apiKey: 'sk-ant-hardcoded' });` },
|
|
36
|
+
// Zero-argument call — the require gate must not choke on it
|
|
37
|
+
{ code: `${IMPORT}\ninitialise();` },
|
|
38
|
+
// Quoted key that is not a credential
|
|
39
|
+
{ code: `${IMPORT}\nnew Anthropic({ 'baseURL': 'https://api.anthropic.com' });` },
|
|
40
|
+
// An import of an unrelated module must not arm the rule
|
|
41
|
+
{ code: `import { z } from 'zod';\nnew Anthropic({ apiKey: 'sk-ant-x' });` },
|
|
42
|
+
],
|
|
43
|
+
invalid: [
|
|
44
|
+
{
|
|
45
|
+
code: `${IMPORT}\nconst c = new Anthropic({ apiKey: 'sk-ant-api03-hardcoded' });`,
|
|
46
|
+
errors: [{ messageId: 'hardcodedApiKey' }],
|
|
47
|
+
},
|
|
48
|
+
// authToken is the same class of credential
|
|
49
|
+
{
|
|
50
|
+
code: `${IMPORT}\nconst c = new Anthropic({ authToken: 'tok-live-123' });`,
|
|
51
|
+
errors: [{ messageId: 'hardcodedApiKey' }],
|
|
52
|
+
},
|
|
53
|
+
// Agent SDK shares the client options
|
|
54
|
+
{
|
|
55
|
+
code: `import { query } from '@anthropic-ai/claude-agent-sdk';\nnew Anthropic({ apiKey: 'sk-ant-x' });`,
|
|
56
|
+
errors: [{ messageId: 'hardcodedApiKey' }],
|
|
57
|
+
},
|
|
58
|
+
// require() arms it
|
|
59
|
+
{
|
|
60
|
+
code: `const Anthropic = require('@anthropic-ai/sdk');\nnew Anthropic({ apiKey: 'sk-ant-x' });`,
|
|
61
|
+
errors: [{ messageId: 'hardcodedApiKey' }],
|
|
62
|
+
},
|
|
63
|
+
// String-literal key
|
|
64
|
+
{
|
|
65
|
+
code: `${IMPORT}\nnew Anthropic({ 'apiKey': 'sk-ant-x' });`,
|
|
66
|
+
errors: [{ messageId: 'hardcodedApiKey' }],
|
|
67
|
+
},
|
|
68
|
+
// Factory-style call
|
|
69
|
+
{
|
|
70
|
+
code: `${IMPORT}\nconst c = createAnthropic({ apiKey: 'sk-ant-x' });`,
|
|
71
|
+
errors: [{ messageId: 'hardcodedApiKey' }],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|