@parasraju/plumber-mcp 1.0.1 → 1.2.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/README.md +100 -6
- package/bin/plumber-mcp.js +99 -13
- package/examples/custom-rules.example.json +20 -0
- package/package.json +12 -4
- package/src/engine.js +1095 -323
package/README.md
CHANGED
|
@@ -83,15 +83,15 @@ Add to `.vscode/mcp.json`:
|
|
|
83
83
|
|
|
84
84
|
| Tool | Description |
|
|
85
85
|
| --- | --- |
|
|
86
|
-
| `plumber_scan` | Scan a local directory or file. Returns a score, grade, file count, and one line per issue with its fix. |
|
|
87
|
-
| `plumber_list_rules` | List all
|
|
88
|
-
| `plumber_scan_text` | Scan a pasted code or config snippet. `filename` is optional and only selects the applicable rules. |
|
|
86
|
+
| `plumber_scan` | Scan a local directory or file. Returns a score, grade, file count, and one line per issue with its fix. Optional `rulesFile`, or `.plumber-rules.json` in the target, adds a rule pack. |
|
|
87
|
+
| `plumber_list_rules` | List all 26 inspection rules with id, severity, title, and fix. Optional `rulesFile` appends pack rules marked `source: pack`. |
|
|
88
|
+
| `plumber_scan_text` | Scan a pasted code or config snippet. `filename` is optional and only selects the applicable rules. Optional `rulesFile` adds a rule pack. |
|
|
89
89
|
|
|
90
90
|
Tool inputs are deliberately small so they cost few tokens:
|
|
91
91
|
|
|
92
|
-
- `plumber_scan`: `target` (string, default `"."`)
|
|
93
|
-
- `plumber_list_rules`:
|
|
94
|
-
- `plumber_scan_text`: `text` (string, required), `filename` (string, optional)
|
|
92
|
+
- `plumber_scan`: `target` (string, default `"."`), `rulesFile` (string, optional)
|
|
93
|
+
- `plumber_list_rules`: `rulesFile` (string, optional)
|
|
94
|
+
- `plumber_scan_text`: `text` (string, required), `filename` (string, optional), `rulesFile` (string, optional)
|
|
95
95
|
|
|
96
96
|
## Example session
|
|
97
97
|
|
|
@@ -103,6 +103,94 @@ Tool inputs are deliberately small so they cost few tokens:
|
|
|
103
103
|
>
|
|
104
104
|
> **Agent:** I moved the key into `.env`, replaced it with `process.env.OPENAI_API_KEY`, and added `.env` to `.gitignore`. You should revoke the exposed key.
|
|
105
105
|
|
|
106
|
+
### Scan GitHub Actions workflows
|
|
107
|
+
|
|
108
|
+
`plumber_scan` reads `.github/workflows/*.yml` and `*.yaml`. `plumber_scan_text` applies the same rules when the filename is under a `workflows` path, or when the snippet parses as a workflow (`on:` and `jobs:`).
|
|
109
|
+
|
|
110
|
+
- `sec/pull-request-target` (CRITICAL) — a `pull_request_target` workflow checks out untrusted pull request code or grants write permissions.
|
|
111
|
+
- `sec/expression-injection` (CRITICAL) — `${{ github.event.* }}`, `${{ github.head_ref }}`, or `${{ inputs.* }}` is interpolated into a `run` script.
|
|
112
|
+
- `pipe/action-unpinned` (MEDIUM) — a third-party `uses:` action is pinned to a branch or version tag instead of a full commit SHA.
|
|
113
|
+
- `sec/job-permissions` (HIGH) — a job that pushes, releases, or deploys has no permissions block, uses `write-all`, or sets `contents: write` where a narrower scope would do.
|
|
114
|
+
- `sec/secret-to-untrusted` (HIGH) — a secret is passed through `env` or an argument to a third-party action.
|
|
115
|
+
|
|
116
|
+
> **Agent:** *(calls `plumber_scan` with `target: "."` on a repo whose `.github/workflows/pr.yml` checks out the pull request head from `pull_request_target`)*
|
|
117
|
+
>
|
|
118
|
+
> `- [CRITICAL] Untrusted pull_request_target Checkout or Write Permission (Use pull_request, or check out the base ref read-only with no secrets.) — .github/workflows/pr.yml:9`
|
|
119
|
+
|
|
120
|
+
### Workflow YAML rules
|
|
121
|
+
|
|
122
|
+
- `sec/pull-request-target` [CRITICAL] Untrusted pull_request_target Checkout or Write Permission (Use pull_request, or check out the base ref read-only with no secrets.)
|
|
123
|
+
- `sec/expression-injection` [CRITICAL] Expression Injection in Workflow run Step (Pass the value through an env: variable and reference that variable in the script.)
|
|
124
|
+
- `pipe/action-unpinned` [MEDIUM] Unpinned Third-Party Action (Pin the action to a full commit SHA.)
|
|
125
|
+
- `sec/job-permissions` [HIGH] Job Writes Without Least-Privilege Permissions (Set least-privilege permissions on the job.)
|
|
126
|
+
- `sec/secret-to-untrusted` [HIGH] Secret Passed to Untrusted Step (Scope the secret to the trusted step that needs it.)
|
|
127
|
+
|
|
128
|
+
### Custom rule packs — bring your own rules
|
|
129
|
+
|
|
130
|
+
Bring rule packs when you need custom rules the built-in set does not ship: an internal secret format, a banned pattern, or a house naming rule. A pack is one local JSON file. This release does not read YAML packs, and it does not download a pack from a URL.
|
|
131
|
+
|
|
132
|
+
The file is a top-level object `{ "rules": [...] }`. Each rule uses these fields:
|
|
133
|
+
|
|
134
|
+
- `id` (string, required) — must start with `custom/`, for example `custom/no-internal-api-key`. Any other id is rejected. An id that matches a built-in rule, or another rule in the same pack, is rejected. The built-in rule is left unchanged.
|
|
135
|
+
- `severity` (string, required) — one of `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`. Anything else is rejected.
|
|
136
|
+
- `title` (string, required) — short name, for example `Internal API key format`.
|
|
137
|
+
- `pattern` (string, required) — a JavaScript regular expression, matched against the file text. A pattern that does not compile rejects that rule.
|
|
138
|
+
- `files` (array of glob strings, optional) — for example `["**/*.js", "**/*.ts"]`. The rule runs only on files that match one of these globs. Omit it to use every file Plumber already scans. Globs support `*`, `?`, and `**`. `*` does not cross a `/`; use `**/*.js` to match JavaScript in any directory.
|
|
139
|
+
- `fix` (string, required) — the plain-English fix, in the same voice as the built-in rules. A rule with no fix is rejected.
|
|
140
|
+
|
|
141
|
+
On `plumber_scan`, globs only filter files the scanner already opens: `.py`, `.js`, `.jsx`, `.ts`, `.tsx`, `.html`, `.htm`, `.css`, `.scss`, `.env*`, and GitHub Actions workflow YAML. `plumber_scan_text` applies the same globs to the `filename` you pass.
|
|
142
|
+
|
|
143
|
+
A bad rule is dropped. The tool result includes a `Pack error:` line that names the rule and the problem (invalid JSON names the pack file, because there is no rule yet). The 26 built-in rules still run. A broken pack does not wipe the report and does not stop the server.
|
|
144
|
+
|
|
145
|
+
One pack is loaded, in this order:
|
|
146
|
+
|
|
147
|
+
1. `rulesFile`, when the tool call includes it. A relative path is resolved from the server's working directory.
|
|
148
|
+
2. Auto-load, only for `plumber_scan`, and only when `rulesFile` is omitted. Plumber reads `.plumber-rules.json` in the directory passed as `target`. A file target uses the directory that contains that file. Parent folders and subfolders are not searched. `plumber_scan_text` and `plumber_list_rules` do not auto-load.
|
|
149
|
+
3. When both are present, `rulesFile` is the pack that runs. The two files are not merged.
|
|
150
|
+
|
|
151
|
+
The scan result names the pack on one line: the file path and how many pack rules loaded, for example `Pack: /repo/.plumber-rules.json (2 pack rules loaded)`.
|
|
152
|
+
|
|
153
|
+
`plumber_list_rules` with no `rulesFile` returns the 26 built-in rules. With `rulesFile`, the pack rules follow the built-ins and each one ends with `source: pack`.
|
|
154
|
+
|
|
155
|
+
Save this file as `.plumber-rules.json` in the directory you scan, or pass the path as `rulesFile`. The same example ships at `examples/custom-rules.example.json`.
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"rules": [
|
|
160
|
+
{
|
|
161
|
+
"id": "custom/no-internal-api-key",
|
|
162
|
+
"severity": "CRITICAL",
|
|
163
|
+
"title": "Internal API key format",
|
|
164
|
+
"pattern": "\\bINT_[A-Z0-9]{16}\\b",
|
|
165
|
+
"files": ["**/*.js", "**/*.ts"],
|
|
166
|
+
"fix": "Move the internal key to an environment variable and rotate it."
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"id": "custom/no-banned-marker",
|
|
170
|
+
"severity": "MEDIUM",
|
|
171
|
+
"title": "Banned ship marker",
|
|
172
|
+
"pattern": "\\bDO_NOT_SHIP\\b",
|
|
173
|
+
"files": ["**/*.js", "**/*.py"],
|
|
174
|
+
"fix": "Remove the banned marker before committing this file."
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Three ways to run that pack:
|
|
181
|
+
|
|
182
|
+
1. Auto-load: write it to `.plumber-rules.json`, then call `plumber_scan` with `{ "target": "." }`.
|
|
183
|
+
2. `rulesFile` on `plumber_scan`: `{ "target": ".", "rulesFile": "examples/custom-rules.example.json" }`. This is the pack that runs when `.plumber-rules.json` is also present.
|
|
184
|
+
3. `rulesFile` on `plumber_scan_text`: `{ "text": "const internal = \"INT_ABCDEFGHIJ123456\";\n", "filename": "src/config.js", "rulesFile": "examples/custom-rules.example.json" }`. A snippet that does not match the pattern produces no pack finding.
|
|
185
|
+
|
|
186
|
+
`src/config.js` containing `INT_ABCDEFGHIJ123456` then reports:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
- [CRITICAL] Internal API key format (Move the internal key to an environment variable and rotate it.) — src/config.js:1
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The secret-format rule above does not check Python files. The banned-marker rule does, because its `files` list says so.
|
|
193
|
+
|
|
106
194
|
## Links
|
|
107
195
|
|
|
108
196
|
- CLI: [plumber-linter](https://www.npmjs.com/package/plumber-linter)
|
|
@@ -112,6 +200,12 @@ Tool inputs are deliberately small so they cost few tokens:
|
|
|
112
200
|
|
|
113
201
|
## Changelog
|
|
114
202
|
|
|
203
|
+
## 1.2.0
|
|
204
|
+
- Custom rule packs: load your own rules from `.plumber-rules.json` or a `rulesFile`, ids under `custom/`.
|
|
205
|
+
|
|
206
|
+
## 1.1.0
|
|
207
|
+
- Workflow YAML rules: sec/pull-request-target, sec/expression-injection, pipe/action-unpinned, sec/job-permissions, sec/secret-to-untrusted.
|
|
208
|
+
|
|
115
209
|
## 1.0.1
|
|
116
210
|
- Fix false-positive test path matching that skipped secrets in files containing "test" or "spec" substrings (e.g. `latest.js`, `inspector.js`).
|
|
117
211
|
- Fix directory scan skipping target path when root directory name matches skip list or begins with a dot (e.g. `dist`, `.hidden`).
|
package/bin/plumber-mcp.js
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Tools:
|
|
7
7
|
* plumber_scan scan a local directory or file
|
|
8
|
-
* plumber_list_rules list
|
|
8
|
+
* plumber_list_rules list every inspection rule
|
|
9
9
|
* plumber_scan_text scan a pasted snippet
|
|
10
10
|
*
|
|
11
|
+
* Each tool accepts an optional rulesFile path to one local JSON rule pack.
|
|
12
|
+
* plumber_scan also reads .plumber-rules.json in the target when rulesFile is omitted.
|
|
13
|
+
* Packs are never fetched from a URL.
|
|
14
|
+
*
|
|
11
15
|
* Only JSON-RPC goes to stdout. Diagnostics go to stderr.
|
|
12
16
|
*/
|
|
13
17
|
|
|
@@ -21,7 +25,7 @@ const {
|
|
|
21
25
|
ErrorCode,
|
|
22
26
|
McpError
|
|
23
27
|
} = require('@modelcontextprotocol/sdk/types.js');
|
|
24
|
-
const { RULES, SCANNABLE_EXTS, scanLocalDirectory, scanText } = require('../src/engine.js');
|
|
28
|
+
const { RULES, SCANNABLE_EXTS, scanLocalDirectory, scanText, loadRulePack, resolveScanPack } = require('../src/engine.js');
|
|
25
29
|
|
|
26
30
|
// v1.0.1 fix: read the version from package.json so serverInfo never drifts.
|
|
27
31
|
// Repro (v1.0.0): initialize -> serverInfo.version was hardcoded to "1.0.0".
|
|
@@ -34,7 +38,8 @@ const TOOLS = [
|
|
|
34
38
|
inputSchema: {
|
|
35
39
|
type: 'object',
|
|
36
40
|
properties: {
|
|
37
|
-
target: { type: 'string', description: 'Directory or file path to scan (default ".")' }
|
|
41
|
+
target: { type: 'string', description: 'Directory or file path to scan (default ".")' },
|
|
42
|
+
rulesFile: { type: 'string', description: 'Optional path to a local JSON rule pack. When omitted, .plumber-rules.json is loaded from the scan target if that file exists.' }
|
|
38
43
|
},
|
|
39
44
|
additionalProperties: false
|
|
40
45
|
}
|
|
@@ -44,7 +49,9 @@ const TOOLS = [
|
|
|
44
49
|
description: 'List all Plumber inspection rules with severity, title, and recommended fix.',
|
|
45
50
|
inputSchema: {
|
|
46
51
|
type: 'object',
|
|
47
|
-
properties: {
|
|
52
|
+
properties: {
|
|
53
|
+
rulesFile: { type: 'string', description: 'Optional path to a local JSON rule pack. Pack rules are listed after the built-in rules and marked as custom.' }
|
|
54
|
+
},
|
|
48
55
|
additionalProperties: false
|
|
49
56
|
}
|
|
50
57
|
},
|
|
@@ -55,7 +62,8 @@ const TOOLS = [
|
|
|
55
62
|
type: 'object',
|
|
56
63
|
properties: {
|
|
57
64
|
text: { type: 'string', description: 'Code or config snippet to scan' },
|
|
58
|
-
filename: { type: 'string', description: 'Optional filename used to select applicable rules (e.g. app.js)' }
|
|
65
|
+
filename: { type: 'string', description: 'Optional filename used to select applicable rules (e.g. app.js)' },
|
|
66
|
+
rulesFile: { type: 'string', description: 'Optional path to a local JSON rule pack applied to this snippet. Pasted snippets do not auto-load a pack.' }
|
|
59
67
|
},
|
|
60
68
|
required: ['text'],
|
|
61
69
|
additionalProperties: false
|
|
@@ -63,22 +71,47 @@ const TOOLS = [
|
|
|
63
71
|
}
|
|
64
72
|
];
|
|
65
73
|
|
|
74
|
+
function packLoadedLine(pack) {
|
|
75
|
+
const count = Array.isArray(pack.rules) ? pack.rules.length : 0;
|
|
76
|
+
const label = count === 1 ? 'pack rule' : 'pack rules';
|
|
77
|
+
return `Pack: ${pack.path} (${count} ${label} loaded)`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatPackBanner(pack) {
|
|
81
|
+
if (!pack) return '';
|
|
82
|
+
const lines = [packLoadedLine(pack)];
|
|
83
|
+
const errors = Array.isArray(pack.errors) ? pack.errors : [];
|
|
84
|
+
for (let i = 0; i < errors.length; i++) {
|
|
85
|
+
lines.push(`Pack error: ${errors[i]}`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatListedRule(rule) {
|
|
91
|
+
const severity = rule.sev || rule.severity;
|
|
92
|
+
const line = `${rule.id} [${severity}] ${rule.title} (${rule.fix})`;
|
|
93
|
+
if (rule.source === 'pack') return `${line} source: pack`;
|
|
94
|
+
return line;
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
function formatReport(report) {
|
|
67
98
|
const header = [
|
|
68
99
|
`Plumber Health Report for ${report.repo_name}`,
|
|
69
100
|
report.total_files === 0 ? 'Score: n/a' : `Score: ${report.score}/100 (Grade ${report.grade})`,
|
|
70
101
|
`Files: ${report.total_files} | Issues: ${report.issues.length}`
|
|
71
102
|
].join('\n');
|
|
103
|
+
const banner = formatPackBanner(report.pack);
|
|
104
|
+
const top = banner ? `${header}\n${banner}` : header;
|
|
72
105
|
|
|
73
106
|
// v1.0.1 fix: do not report "No issues found." when nothing was scanned.
|
|
74
107
|
// Repro (v1.0.0): plumber_scan { target: ".github/workflows/ci.yml" } (or a
|
|
75
108
|
// Dockerfile, or an empty dir) -> "Score: 100/100 (Grade A) ... No issues found."
|
|
76
109
|
if (report.total_files === 0) {
|
|
77
|
-
return `${
|
|
110
|
+
return `${top}\n\nNo scannable files found, so nothing was checked. Supported: ${SCANNABLE_EXTS.join(' ')} and .env* files.`;
|
|
78
111
|
}
|
|
79
112
|
|
|
80
113
|
if (report.issues.length === 0) {
|
|
81
|
-
return `${
|
|
114
|
+
return `${top}\n\nNo issues found.`;
|
|
82
115
|
}
|
|
83
116
|
|
|
84
117
|
const locations = new Map();
|
|
@@ -94,7 +127,7 @@ function formatReport(report) {
|
|
|
94
127
|
return `- [${issue.severity}] ${issue.title} (${issue.fix})${suffix}`;
|
|
95
128
|
});
|
|
96
129
|
|
|
97
|
-
return `${
|
|
130
|
+
return `${top}\n\n${lines.join('\n')}`;
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
function requireString(value, name) {
|
|
@@ -114,15 +147,29 @@ function requireText(text) {
|
|
|
114
147
|
|
|
115
148
|
function runScanTool(args) {
|
|
116
149
|
const target = requireString(args.target, 'target') || '.';
|
|
150
|
+
const rulesFile = requireString(args.rulesFile, 'rulesFile');
|
|
117
151
|
const absolute = path.resolve(target);
|
|
118
152
|
|
|
119
153
|
if (!fs.existsSync(absolute)) {
|
|
120
154
|
throw new McpError(ErrorCode.InvalidParams, `Target path does not exist: ${absolute}`);
|
|
121
155
|
}
|
|
122
156
|
|
|
157
|
+
let pack = null;
|
|
123
158
|
try {
|
|
124
|
-
|
|
159
|
+
pack = resolveScanPack(absolute, rulesFile);
|
|
125
160
|
} catch (err) {
|
|
161
|
+
pack = {
|
|
162
|
+
path: rulesFile || absolute,
|
|
163
|
+
rules: [],
|
|
164
|
+
errors: [`Pack failed to load: ${err.message}`],
|
|
165
|
+
broken: true
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
return formatReport(scanLocalDirectory(absolute, pack ? { pack } : undefined));
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (err instanceof McpError) throw err;
|
|
126
173
|
throw new McpError(ErrorCode.InternalError, `Scan failed: ${err.message}`);
|
|
127
174
|
}
|
|
128
175
|
}
|
|
@@ -130,16 +177,55 @@ function runScanTool(args) {
|
|
|
130
177
|
function runScanTextTool(args) {
|
|
131
178
|
const text = requireText(args.text);
|
|
132
179
|
const filename = requireString(args.filename, 'filename');
|
|
180
|
+
const rulesFile = requireString(args.rulesFile, 'rulesFile');
|
|
181
|
+
let pack = null;
|
|
182
|
+
if (rulesFile) {
|
|
183
|
+
try {
|
|
184
|
+
pack = loadRulePack(rulesFile);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
pack = {
|
|
187
|
+
path: rulesFile,
|
|
188
|
+
rules: [],
|
|
189
|
+
errors: [`Pack ${rulesFile} failed to load: ${err.message}`],
|
|
190
|
+
broken: true
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
133
194
|
|
|
134
195
|
try {
|
|
135
|
-
return formatReport(scanText(text, filename));
|
|
196
|
+
return formatReport(scanText(text, filename, pack ? { pack } : undefined));
|
|
136
197
|
} catch (err) {
|
|
198
|
+
if (err instanceof McpError) throw err;
|
|
137
199
|
throw new McpError(ErrorCode.InternalError, `Scan failed: ${err.message}`);
|
|
138
200
|
}
|
|
139
201
|
}
|
|
140
202
|
|
|
141
|
-
function runListRulesTool() {
|
|
142
|
-
|
|
203
|
+
function runListRulesTool(args) {
|
|
204
|
+
const rulesFile = requireString(args && args.rulesFile, 'rulesFile');
|
|
205
|
+
let pack = null;
|
|
206
|
+
if (rulesFile) {
|
|
207
|
+
try {
|
|
208
|
+
pack = loadRulePack(rulesFile);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
pack = {
|
|
211
|
+
path: rulesFile,
|
|
212
|
+
rules: [],
|
|
213
|
+
errors: [`Pack ${rulesFile} failed to load: ${err.message}`],
|
|
214
|
+
broken: true
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const lines = RULES.map((rule) => formatListedRule(rule));
|
|
219
|
+
if (!pack) return lines.join('\n');
|
|
220
|
+
for (let i = 0; i < pack.rules.length; i++) {
|
|
221
|
+
lines.push(formatListedRule(pack.rules[i]));
|
|
222
|
+
}
|
|
223
|
+
lines.push(packLoadedLine(pack));
|
|
224
|
+
const errors = pack.errors || [];
|
|
225
|
+
for (let i = 0; i < errors.length; i++) {
|
|
226
|
+
lines.push(`Pack error: ${errors[i]}`);
|
|
227
|
+
}
|
|
228
|
+
return lines.join('\n');
|
|
143
229
|
}
|
|
144
230
|
|
|
145
231
|
async function main() {
|
|
@@ -155,7 +241,7 @@ async function main() {
|
|
|
155
241
|
if (name === 'plumber_scan') {
|
|
156
242
|
text = runScanTool(args);
|
|
157
243
|
} else if (name === 'plumber_list_rules') {
|
|
158
|
-
text = runListRulesTool();
|
|
244
|
+
text = runListRulesTool(args);
|
|
159
245
|
} else if (name === 'plumber_scan_text') {
|
|
160
246
|
text = runScanTextTool(args);
|
|
161
247
|
} else {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rules": [
|
|
3
|
+
{
|
|
4
|
+
"id": "custom/no-internal-api-key",
|
|
5
|
+
"severity": "CRITICAL",
|
|
6
|
+
"title": "Internal API key format",
|
|
7
|
+
"pattern": "\\bINT_[A-Z0-9]{16}\\b",
|
|
8
|
+
"files": ["**/*.js", "**/*.ts"],
|
|
9
|
+
"fix": "Move the internal key to an environment variable and rotate it."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "custom/no-banned-marker",
|
|
13
|
+
"severity": "MEDIUM",
|
|
14
|
+
"title": "Banned ship marker",
|
|
15
|
+
"pattern": "\\bDO_NOT_SHIP\\b",
|
|
16
|
+
"files": ["**/*.js", "**/*.py"],
|
|
17
|
+
"fix": "Remove the banned marker before committing this file."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parasraju/plumber-mcp",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Plumber as an MCP server — let your AI coding agent scan repos for leaked secrets, dead code, and AI slop, with fixes in plain English.",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Plumber as an MCP server — let your AI coding agent scan repos and GitHub Actions workflows for leaked secrets, dead code, and AI slop, with fixes in plain English. Add your own checks with custom rule packs.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"plumber-mcp": "bin/plumber-mcp.js"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"bin",
|
|
17
17
|
"src",
|
|
18
|
+
"examples",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
|
20
21
|
"keywords": [
|
|
@@ -27,7 +28,13 @@
|
|
|
27
28
|
"security",
|
|
28
29
|
"secrets-detection",
|
|
29
30
|
"code-quality",
|
|
30
|
-
"linter"
|
|
31
|
+
"linter",
|
|
32
|
+
"github-actions",
|
|
33
|
+
"ci-cd",
|
|
34
|
+
"workflow",
|
|
35
|
+
"yaml",
|
|
36
|
+
"custom-rules",
|
|
37
|
+
"rule-packs"
|
|
31
38
|
],
|
|
32
39
|
"repository": {
|
|
33
40
|
"type": "git",
|
|
@@ -40,6 +47,7 @@
|
|
|
40
47
|
"license": "MIT",
|
|
41
48
|
"author": "Paras Raju",
|
|
42
49
|
"dependencies": {
|
|
43
|
-
"@modelcontextprotocol/sdk": "^1.32.2"
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
51
|
+
"js-yaml": "^4.3.2"
|
|
44
52
|
}
|
|
45
53
|
}
|