agent-mcp-guard 0.1.1 → 0.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 +17 -2
- package/action.yml +111 -0
- package/docs/business-playbook.md +2 -2
- package/docs/github-action.md +68 -0
- package/docs/roadmap.md +10 -12
- package/package.json +2 -1
- package/src/cli.js +9 -5
- package/src/report.js +461 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Website: [chaoyue0307.github.io/mcp-guard](https://chaoyue0307.github.io/mcp-gua
|
|
|
14
14
|
<a href="https://www.npmjs.com/package/agent-mcp-guard"><img alt="npm version" src="https://img.shields.io/npm/v/agent-mcp-guard?color=0f766e"></a>
|
|
15
15
|
<a href="https://github.com/ChaoYue0307/mcp-guard/actions"><img alt="CI" src="https://github.com/ChaoYue0307/mcp-guard/actions/workflows/ci.yml/badge.svg"></a>
|
|
16
16
|
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-111827"></a>
|
|
17
|
-
<a href="https://github.com/ChaoYue0307/mcp-guard/releases/tag/v0.
|
|
17
|
+
<a href="https://github.com/ChaoYue0307/mcp-guard/releases/tag/v0.2.0"><img alt="Release" src="https://img.shields.io/github/v/release/ChaoYue0307/mcp-guard?color=7c2d12"></a>
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
## Install
|
|
@@ -36,12 +36,26 @@ Generate a Markdown report:
|
|
|
36
36
|
mcp-guard scan --format markdown --output mcp-guard-report.md
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
Generate an HTML report:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
mcp-guard scan --format html --output mcp-guard-report.html
|
|
43
|
+
```
|
|
44
|
+
|
|
39
45
|
Use in CI:
|
|
40
46
|
|
|
41
47
|
```bash
|
|
42
48
|
mcp-guard scan --config .mcp.json --fail-on high
|
|
43
49
|
```
|
|
44
50
|
|
|
51
|
+
Use the GitHub Action:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
- uses: ChaoYue0307/mcp-guard@v0.2.0
|
|
55
|
+
with:
|
|
56
|
+
fail-on: high
|
|
57
|
+
```
|
|
58
|
+
|
|
45
59
|
## What It Finds
|
|
46
60
|
|
|
47
61
|
| Risk | Why it matters |
|
|
@@ -98,7 +112,7 @@ MCP configs often contain sensitive local paths, internal hostnames, tokens, and
|
|
|
98
112
|
- no config upload;
|
|
99
113
|
- no external API call;
|
|
100
114
|
- secret-like values redacted in reports;
|
|
101
|
-
- text, Markdown, and JSON output for local review and CI.
|
|
115
|
+
- text, Markdown, HTML, and JSON output for local review and CI.
|
|
102
116
|
|
|
103
117
|
## Commercial Support
|
|
104
118
|
|
|
@@ -113,6 +127,7 @@ Service details: [docs/paid-audit.md](docs/paid-audit.md)
|
|
|
113
127
|
## Documentation
|
|
114
128
|
|
|
115
129
|
- [Rule reference](docs/rules.md)
|
|
130
|
+
- [GitHub Action](docs/github-action.md)
|
|
116
131
|
- [Privacy and security](docs/privacy-and-security.md)
|
|
117
132
|
- [Roadmap](docs/roadmap.md)
|
|
118
133
|
- [Business playbook](docs/business-playbook.md)
|
package/action.yml
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
name: mcp-guard
|
|
2
|
+
description: Scan MCP and AI agent tool configuration for risky commands, secrets, and broad permissions.
|
|
3
|
+
author: mcp-guard
|
|
4
|
+
|
|
5
|
+
branding:
|
|
6
|
+
icon: shield
|
|
7
|
+
color: green
|
|
8
|
+
|
|
9
|
+
inputs:
|
|
10
|
+
config:
|
|
11
|
+
description: Optional MCP config path to scan. Leave empty to scan default project and user config locations.
|
|
12
|
+
required: false
|
|
13
|
+
default: ""
|
|
14
|
+
fail-on:
|
|
15
|
+
description: Fail the workflow when a finding is at least this severity. Use critical, high, medium, low, or none.
|
|
16
|
+
required: false
|
|
17
|
+
default: high
|
|
18
|
+
output-dir:
|
|
19
|
+
description: Directory where reports will be written.
|
|
20
|
+
required: false
|
|
21
|
+
default: mcp-guard-report
|
|
22
|
+
package-version:
|
|
23
|
+
description: npm package version to install.
|
|
24
|
+
required: false
|
|
25
|
+
default: latest
|
|
26
|
+
upload-artifact:
|
|
27
|
+
description: Upload generated reports as a workflow artifact.
|
|
28
|
+
required: false
|
|
29
|
+
default: "true"
|
|
30
|
+
artifact-name:
|
|
31
|
+
description: Artifact name for generated reports.
|
|
32
|
+
required: false
|
|
33
|
+
default: mcp-guard-report
|
|
34
|
+
|
|
35
|
+
outputs:
|
|
36
|
+
markdown-report:
|
|
37
|
+
description: Path to the generated Markdown report.
|
|
38
|
+
value: ${{ steps.reports.outputs.markdown-report }}
|
|
39
|
+
html-report:
|
|
40
|
+
description: Path to the generated HTML report.
|
|
41
|
+
value: ${{ steps.reports.outputs.html-report }}
|
|
42
|
+
json-report:
|
|
43
|
+
description: Path to the generated JSON report.
|
|
44
|
+
value: ${{ steps.reports.outputs.json-report }}
|
|
45
|
+
exit-code:
|
|
46
|
+
description: mcp-guard threshold exit code.
|
|
47
|
+
value: ${{ steps.reports.outputs.exit-code }}
|
|
48
|
+
|
|
49
|
+
runs:
|
|
50
|
+
using: composite
|
|
51
|
+
steps:
|
|
52
|
+
- name: Set up Node.js
|
|
53
|
+
uses: actions/setup-node@v4
|
|
54
|
+
with:
|
|
55
|
+
node-version: "20"
|
|
56
|
+
|
|
57
|
+
- name: Install mcp-guard
|
|
58
|
+
shell: bash
|
|
59
|
+
env:
|
|
60
|
+
MCP_GUARD_PACKAGE_VERSION: ${{ inputs.package-version }}
|
|
61
|
+
run: npm install --global "agent-mcp-guard@${MCP_GUARD_PACKAGE_VERSION}"
|
|
62
|
+
|
|
63
|
+
- name: Generate reports
|
|
64
|
+
id: reports
|
|
65
|
+
shell: bash
|
|
66
|
+
env:
|
|
67
|
+
MCP_GUARD_CONFIG: ${{ inputs.config }}
|
|
68
|
+
MCP_GUARD_FAIL_ON: ${{ inputs.fail-on }}
|
|
69
|
+
MCP_GUARD_OUTPUT_DIR: ${{ inputs.output-dir }}
|
|
70
|
+
run: |
|
|
71
|
+
set -euo pipefail
|
|
72
|
+
|
|
73
|
+
mkdir -p "${MCP_GUARD_OUTPUT_DIR}"
|
|
74
|
+
|
|
75
|
+
scan_args=()
|
|
76
|
+
if [ -n "${MCP_GUARD_CONFIG}" ]; then
|
|
77
|
+
scan_args+=(--config "${MCP_GUARD_CONFIG}")
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
markdown_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.md"
|
|
81
|
+
html_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.html"
|
|
82
|
+
json_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.json"
|
|
83
|
+
|
|
84
|
+
mcp-guard scan "${scan_args[@]}" --format markdown --output "${markdown_report}" --fail-on none
|
|
85
|
+
mcp-guard scan "${scan_args[@]}" --format html --output "${html_report}" --fail-on none
|
|
86
|
+
mcp-guard scan "${scan_args[@]}" --format json --output "${json_report}" --fail-on none
|
|
87
|
+
|
|
88
|
+
set +e
|
|
89
|
+
mcp-guard scan "${scan_args[@]}" --fail-on "${MCP_GUARD_FAIL_ON}"
|
|
90
|
+
status="$?"
|
|
91
|
+
set -e
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
echo "markdown-report=${markdown_report}"
|
|
95
|
+
echo "html-report=${html_report}"
|
|
96
|
+
echo "json-report=${json_report}"
|
|
97
|
+
echo "exit-code=${status}"
|
|
98
|
+
} >> "${GITHUB_OUTPUT}"
|
|
99
|
+
|
|
100
|
+
- name: Upload report artifact
|
|
101
|
+
if: ${{ always() && inputs.upload-artifact == 'true' }}
|
|
102
|
+
uses: actions/upload-artifact@v4
|
|
103
|
+
with:
|
|
104
|
+
name: ${{ inputs.artifact-name }}
|
|
105
|
+
path: ${{ inputs.output-dir }}
|
|
106
|
+
|
|
107
|
+
- name: Enforce severity threshold
|
|
108
|
+
shell: bash
|
|
109
|
+
env:
|
|
110
|
+
MCP_GUARD_EXIT_CODE: ${{ steps.reports.outputs.exit-code }}
|
|
111
|
+
run: exit "${MCP_GUARD_EXIT_CODE}"
|
|
@@ -13,9 +13,10 @@ AI Agent/MCP Security Audit.
|
|
|
13
13
|
Deliverables:
|
|
14
14
|
|
|
15
15
|
- MCP server inventory;
|
|
16
|
-
- `mcp-guard` scan
|
|
16
|
+
- `mcp-guard` Markdown, HTML, and JSON scan reports;
|
|
17
17
|
- manual review of high-risk findings;
|
|
18
18
|
- prioritized remediation plan;
|
|
19
|
+
- optional GitHub Action setup for continuous scans;
|
|
19
20
|
- 60-minute hardening call;
|
|
20
21
|
- optional PR with safer config changes.
|
|
21
22
|
|
|
@@ -61,4 +62,3 @@ Weak:
|
|
|
61
62
|
- vague security interest;
|
|
62
63
|
- requests for a full dashboard before any audit;
|
|
63
64
|
- only free users with toy configs.
|
|
64
|
-
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# GitHub Action
|
|
2
|
+
|
|
3
|
+
Use the `mcp-guard` action to scan MCP and AI agent tool configuration in pull requests and CI.
|
|
4
|
+
|
|
5
|
+
The action installs the published npm package, generates Markdown, HTML, and JSON reports, uploads them as a workflow artifact, then fails the job when findings meet your selected severity threshold.
|
|
6
|
+
|
|
7
|
+
## Basic Workflow
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
name: mcp-guard
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
pull_request:
|
|
14
|
+
push:
|
|
15
|
+
branches: [main]
|
|
16
|
+
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
scan:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: ChaoYue0307/mcp-guard@v0.2.0
|
|
26
|
+
with:
|
|
27
|
+
fail-on: high
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Scan a Specific Config
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
- uses: ChaoYue0307/mcp-guard@v0.2.0
|
|
34
|
+
with:
|
|
35
|
+
config: .mcp.json
|
|
36
|
+
fail-on: medium
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Pin the npm Package
|
|
40
|
+
|
|
41
|
+
The action defaults to `agent-mcp-guard@latest`. Pin it when you want deterministic CI behavior:
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
- uses: ChaoYue0307/mcp-guard@v0.2.0
|
|
45
|
+
with:
|
|
46
|
+
package-version: 0.2.0
|
|
47
|
+
fail-on: high
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Inputs
|
|
51
|
+
|
|
52
|
+
| Input | Default | Description |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `config` | empty | Optional MCP config path. Empty scans default project and user config locations. |
|
|
55
|
+
| `fail-on` | `high` | Fails the job for `critical`, `high`, `medium`, or `low` findings. Use `none` for report-only mode. |
|
|
56
|
+
| `output-dir` | `mcp-guard-report` | Directory for generated reports. |
|
|
57
|
+
| `package-version` | `latest` | npm package version to install. |
|
|
58
|
+
| `upload-artifact` | `true` | Uploads generated reports as a workflow artifact. |
|
|
59
|
+
| `artifact-name` | `mcp-guard-report` | Name of the uploaded artifact. |
|
|
60
|
+
|
|
61
|
+
## Outputs
|
|
62
|
+
|
|
63
|
+
| Output | Description |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `markdown-report` | Path to the generated Markdown report. |
|
|
66
|
+
| `html-report` | Path to the generated HTML report. |
|
|
67
|
+
| `json-report` | Path to the generated JSON report. |
|
|
68
|
+
| `exit-code` | `0` when below threshold, `2` when findings met the threshold. |
|
package/docs/roadmap.md
CHANGED
|
@@ -5,25 +5,24 @@
|
|
|
5
5
|
## Now
|
|
6
6
|
|
|
7
7
|
- CLI config scanning.
|
|
8
|
-
- Text, Markdown, and redacted JSON output.
|
|
8
|
+
- Text, Markdown, HTML, and redacted JSON output.
|
|
9
9
|
- Rules for shell wrappers, remote package runners, unpinned packages, broad filesystem access, secret-like env vars/headers, and remote MCP URLs.
|
|
10
10
|
- CI usage with `--fail-on`.
|
|
11
|
+
- GitHub Action wrapper that uploads Markdown, HTML, and JSON reports as artifacts.
|
|
11
12
|
|
|
12
13
|
## Next
|
|
13
14
|
|
|
14
|
-
1.
|
|
15
|
-
2.
|
|
16
|
-
3.
|
|
17
|
-
4.
|
|
18
|
-
5.
|
|
15
|
+
1. More MCP client discovery paths.
|
|
16
|
+
2. Rule packs mapped to MCP security best practices.
|
|
17
|
+
3. `mcp-guard audit` mode for client-ready reports.
|
|
18
|
+
4. Policy file for approved commands, packages, directories, and remote URLs.
|
|
19
|
+
5. Baseline mode: accept known findings and fail only on new risks.
|
|
19
20
|
|
|
20
21
|
## Later
|
|
21
22
|
|
|
22
|
-
1.
|
|
23
|
-
2.
|
|
24
|
-
3.
|
|
25
|
-
4. Local web report viewer.
|
|
26
|
-
5. Hosted team dashboard only after repeated paid audit demand.
|
|
23
|
+
1. SBOM/package metadata checks for MCP server packages.
|
|
24
|
+
2. Local web report viewer.
|
|
25
|
+
3. Hosted team dashboard only after repeated paid audit demand.
|
|
27
26
|
|
|
28
27
|
## Product Principles
|
|
29
28
|
|
|
@@ -32,4 +31,3 @@
|
|
|
32
31
|
- Avoid noisy rules that do not change behavior.
|
|
33
32
|
- Prefer workflow integration over dashboards.
|
|
34
33
|
- Services first, SaaS later.
|
|
35
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-mcp-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Open-source CLI scanner for risky MCP server and AI agent tool configuration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://chaoyue0307.github.io/mcp-guard/",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"bin",
|
|
41
41
|
"src",
|
|
42
42
|
"README.md",
|
|
43
|
+
"action.yml",
|
|
43
44
|
"LICENSE",
|
|
44
45
|
"SECURITY.md",
|
|
45
46
|
"docs",
|
package/src/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { scan } from "./scan.js";
|
|
4
|
-
import { generateJsonReport, generateMarkdownReport, generateTextReport } from "./report.js";
|
|
4
|
+
import { generateHtmlReport, generateJsonReport, generateMarkdownReport, generateTextReport } from "./report.js";
|
|
5
5
|
import { compareSeverity, severityRank } from "./severity.js";
|
|
6
6
|
|
|
7
|
-
const VERSION = "0.
|
|
7
|
+
const VERSION = "0.2.0";
|
|
8
8
|
|
|
9
9
|
export async function runCli(argv, io) {
|
|
10
10
|
const args = argv.slice(2);
|
|
@@ -80,8 +80,8 @@ function parseScanArgs(args, defaultCwd) {
|
|
|
80
80
|
} else if (arg === "--format" || arg === "-f") {
|
|
81
81
|
options.format = readValue(args, index, arg);
|
|
82
82
|
index += 1;
|
|
83
|
-
if (!["text", "markdown", "json"].includes(options.format)) {
|
|
84
|
-
throw new Error("--format must be one of: text, markdown, json");
|
|
83
|
+
if (!["text", "markdown", "json", "html"].includes(options.format)) {
|
|
84
|
+
throw new Error("--format must be one of: text, markdown, json, html");
|
|
85
85
|
}
|
|
86
86
|
} else if (arg === "--fail-on") {
|
|
87
87
|
options.failOn = readValue(args, index, arg);
|
|
@@ -121,6 +121,9 @@ function renderReport(result, format) {
|
|
|
121
121
|
if (format === "markdown") {
|
|
122
122
|
return generateMarkdownReport(result);
|
|
123
123
|
}
|
|
124
|
+
if (format === "html") {
|
|
125
|
+
return generateHtmlReport(result);
|
|
126
|
+
}
|
|
124
127
|
return generateTextReport(result);
|
|
125
128
|
}
|
|
126
129
|
|
|
@@ -142,7 +145,7 @@ Usage:
|
|
|
142
145
|
Scan options:
|
|
143
146
|
-c, --config <path> Scan a specific MCP config file. Can be repeated.
|
|
144
147
|
-o, --output <path> Write report to a file.
|
|
145
|
-
-f, --format <format> text, markdown, or
|
|
148
|
+
-f, --format <format> text, markdown, json, or html. Default: text.
|
|
146
149
|
--fail-on <severity> Exit 2 when finding severity is at least threshold.
|
|
147
150
|
critical, high, medium, low, none. Default: none.
|
|
148
151
|
--cwd <path> Working directory for project config discovery.
|
|
@@ -151,6 +154,7 @@ Scan options:
|
|
|
151
154
|
Examples:
|
|
152
155
|
mcp-guard scan
|
|
153
156
|
mcp-guard scan --format markdown --output mcp-guard-report.md
|
|
157
|
+
mcp-guard scan --format html --output mcp-guard-report.html
|
|
154
158
|
mcp-guard scan --config .mcp.json --fail-on high
|
|
155
159
|
`;
|
|
156
160
|
}
|
package/src/report.js
CHANGED
|
@@ -105,6 +105,362 @@ export function generateJsonReport(result) {
|
|
|
105
105
|
return JSON.stringify(sanitizeResult(result), null, 2);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export function generateHtmlReport(result) {
|
|
109
|
+
const safeResult = sanitizeResult(result);
|
|
110
|
+
const riskTone = riskToneForScore(safeResult.summary.riskScore);
|
|
111
|
+
const findings = safeResult.findings;
|
|
112
|
+
const servers = safeResult.servers;
|
|
113
|
+
|
|
114
|
+
return `<!doctype html>
|
|
115
|
+
<html lang="en">
|
|
116
|
+
<head>
|
|
117
|
+
<meta charset="utf-8">
|
|
118
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
119
|
+
<title>mcp-guard Scan Report</title>
|
|
120
|
+
<style>
|
|
121
|
+
:root {
|
|
122
|
+
color-scheme: light;
|
|
123
|
+
--bg: #f8fafc;
|
|
124
|
+
--panel: #ffffff;
|
|
125
|
+
--ink: #111827;
|
|
126
|
+
--muted: #5b6575;
|
|
127
|
+
--line: #d9e2ec;
|
|
128
|
+
--soft: #eef4f7;
|
|
129
|
+
--critical: #b91c1c;
|
|
130
|
+
--high: #c2410c;
|
|
131
|
+
--medium: #a16207;
|
|
132
|
+
--low: #0f766e;
|
|
133
|
+
--info: #1d4ed8;
|
|
134
|
+
--shadow: 0 20px 55px rgba(15, 23, 42, 0.08);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
* { box-sizing: border-box; }
|
|
138
|
+
|
|
139
|
+
body {
|
|
140
|
+
margin: 0;
|
|
141
|
+
background: var(--bg);
|
|
142
|
+
color: var(--ink);
|
|
143
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
144
|
+
line-height: 1.5;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
main {
|
|
148
|
+
width: min(1120px, calc(100% - 32px));
|
|
149
|
+
margin: 0 auto;
|
|
150
|
+
padding: 28px 0 48px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.hero {
|
|
154
|
+
background: #ffffff;
|
|
155
|
+
border: 1px solid var(--line);
|
|
156
|
+
border-radius: 8px;
|
|
157
|
+
box-shadow: var(--shadow);
|
|
158
|
+
padding: 28px;
|
|
159
|
+
display: grid;
|
|
160
|
+
grid-template-columns: minmax(0, 1fr) 240px;
|
|
161
|
+
gap: 24px;
|
|
162
|
+
align-items: stretch;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.eyebrow {
|
|
166
|
+
margin: 0 0 8px;
|
|
167
|
+
color: var(--info);
|
|
168
|
+
font-size: 13px;
|
|
169
|
+
font-weight: 700;
|
|
170
|
+
letter-spacing: 0;
|
|
171
|
+
text-transform: uppercase;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
h1, h2 {
|
|
175
|
+
letter-spacing: 0;
|
|
176
|
+
line-height: 1.08;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
h1 {
|
|
180
|
+
margin: 0;
|
|
181
|
+
font-size: 34px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
h2 {
|
|
185
|
+
margin: 0 0 14px;
|
|
186
|
+
font-size: 21px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.lead {
|
|
190
|
+
max-width: 720px;
|
|
191
|
+
margin: 12px 0 0;
|
|
192
|
+
color: var(--muted);
|
|
193
|
+
font-size: 16px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.scorecard {
|
|
197
|
+
border-radius: 8px;
|
|
198
|
+
border: 1px solid var(--line);
|
|
199
|
+
background: var(--soft);
|
|
200
|
+
padding: 18px;
|
|
201
|
+
min-height: 176px;
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-direction: column;
|
|
204
|
+
justify-content: space-between;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.score-label {
|
|
208
|
+
margin: 0;
|
|
209
|
+
color: var(--muted);
|
|
210
|
+
font-size: 13px;
|
|
211
|
+
font-weight: 700;
|
|
212
|
+
text-transform: uppercase;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.score-value {
|
|
216
|
+
margin: 8px 0;
|
|
217
|
+
font-size: 58px;
|
|
218
|
+
font-weight: 800;
|
|
219
|
+
line-height: 1;
|
|
220
|
+
color: var(--${riskTone});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.score-caption {
|
|
224
|
+
margin: 0;
|
|
225
|
+
color: var(--muted);
|
|
226
|
+
font-size: 14px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.grid {
|
|
230
|
+
display: grid;
|
|
231
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
232
|
+
gap: 12px;
|
|
233
|
+
margin: 18px 0 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.metric {
|
|
237
|
+
background: var(--panel);
|
|
238
|
+
border: 1px solid var(--line);
|
|
239
|
+
border-radius: 8px;
|
|
240
|
+
padding: 14px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.metric strong {
|
|
244
|
+
display: block;
|
|
245
|
+
font-size: 24px;
|
|
246
|
+
line-height: 1.1;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.metric span {
|
|
250
|
+
display: block;
|
|
251
|
+
margin-top: 6px;
|
|
252
|
+
color: var(--muted);
|
|
253
|
+
font-size: 13px;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
section {
|
|
257
|
+
margin-top: 22px;
|
|
258
|
+
background: var(--panel);
|
|
259
|
+
border: 1px solid var(--line);
|
|
260
|
+
border-radius: 8px;
|
|
261
|
+
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.04);
|
|
262
|
+
padding: 22px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.severity-row {
|
|
266
|
+
display: grid;
|
|
267
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
268
|
+
gap: 10px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.severity {
|
|
272
|
+
border: 1px solid var(--line);
|
|
273
|
+
border-radius: 8px;
|
|
274
|
+
padding: 12px;
|
|
275
|
+
min-height: 78px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.severity b {
|
|
279
|
+
display: block;
|
|
280
|
+
font-size: 22px;
|
|
281
|
+
line-height: 1.1;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.severity span {
|
|
285
|
+
display: block;
|
|
286
|
+
margin-top: 6px;
|
|
287
|
+
color: var(--muted);
|
|
288
|
+
font-size: 13px;
|
|
289
|
+
text-transform: capitalize;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.critical { color: var(--critical); }
|
|
293
|
+
.high { color: var(--high); }
|
|
294
|
+
.medium { color: var(--medium); }
|
|
295
|
+
.low { color: var(--low); }
|
|
296
|
+
|
|
297
|
+
.table-wrap {
|
|
298
|
+
width: 100%;
|
|
299
|
+
overflow-x: auto;
|
|
300
|
+
border: 1px solid var(--line);
|
|
301
|
+
border-radius: 8px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
table {
|
|
305
|
+
width: 100%;
|
|
306
|
+
min-width: 760px;
|
|
307
|
+
border-collapse: collapse;
|
|
308
|
+
background: var(--panel);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
th, td {
|
|
312
|
+
padding: 12px 14px;
|
|
313
|
+
border-bottom: 1px solid var(--line);
|
|
314
|
+
text-align: left;
|
|
315
|
+
vertical-align: top;
|
|
316
|
+
font-size: 14px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
th {
|
|
320
|
+
color: #374151;
|
|
321
|
+
background: #f1f5f9;
|
|
322
|
+
font-size: 12px;
|
|
323
|
+
text-transform: uppercase;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
tr:last-child td { border-bottom: 0; }
|
|
327
|
+
|
|
328
|
+
code {
|
|
329
|
+
padding: 2px 5px;
|
|
330
|
+
border-radius: 5px;
|
|
331
|
+
background: #eef2f7;
|
|
332
|
+
color: #111827;
|
|
333
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
334
|
+
font-size: 0.92em;
|
|
335
|
+
white-space: normal;
|
|
336
|
+
overflow-wrap: anywhere;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.pill {
|
|
340
|
+
display: inline-flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
min-height: 24px;
|
|
343
|
+
padding: 3px 8px;
|
|
344
|
+
border-radius: 999px;
|
|
345
|
+
background: #f1f5f9;
|
|
346
|
+
font-size: 12px;
|
|
347
|
+
font-weight: 700;
|
|
348
|
+
text-transform: uppercase;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.pill.critical { background: #fee2e2; }
|
|
352
|
+
.pill.high { background: #ffedd5; }
|
|
353
|
+
.pill.medium { background: #fef3c7; }
|
|
354
|
+
.pill.low { background: #ccfbf1; }
|
|
355
|
+
|
|
356
|
+
.empty {
|
|
357
|
+
margin: 0;
|
|
358
|
+
color: var(--muted);
|
|
359
|
+
border: 1px dashed var(--line);
|
|
360
|
+
border-radius: 8px;
|
|
361
|
+
padding: 16px;
|
|
362
|
+
background: #fbfdff;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.notes {
|
|
366
|
+
color: var(--muted);
|
|
367
|
+
font-size: 14px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.notes ul {
|
|
371
|
+
margin: 10px 0 0;
|
|
372
|
+
padding-left: 18px;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@media (max-width: 780px) {
|
|
376
|
+
main {
|
|
377
|
+
width: min(100% - 20px, 1120px);
|
|
378
|
+
padding-top: 10px;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.hero {
|
|
382
|
+
grid-template-columns: 1fr;
|
|
383
|
+
padding: 20px;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
h1 { font-size: 28px; }
|
|
387
|
+
|
|
388
|
+
.grid,
|
|
389
|
+
.severity-row {
|
|
390
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@media (max-width: 460px) {
|
|
395
|
+
.grid,
|
|
396
|
+
.severity-row {
|
|
397
|
+
grid-template-columns: 1fr;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
</style>
|
|
401
|
+
</head>
|
|
402
|
+
<body>
|
|
403
|
+
<main>
|
|
404
|
+
<header class="hero">
|
|
405
|
+
<div>
|
|
406
|
+
<p class="eyebrow">mcp-guard scan report</p>
|
|
407
|
+
<h1>AI agent tool risk review</h1>
|
|
408
|
+
<p class="lead">Local-first review of MCP server configuration, startup commands, remote endpoints, filesystem scope, and secret-like values.</p>
|
|
409
|
+
<div class="grid">
|
|
410
|
+
${metric("Scanned files", safeResult.summary.scannedFileCount)}
|
|
411
|
+
${metric("MCP servers", safeResult.summary.serverCount)}
|
|
412
|
+
${metric("Findings", safeResult.summary.findingCount)}
|
|
413
|
+
${metric("Generated", formatDate(safeResult.metadata.generatedAt))}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
<aside class="scorecard" aria-label="Risk score">
|
|
417
|
+
<div>
|
|
418
|
+
<p class="score-label">Risk score</p>
|
|
419
|
+
<p class="score-value">${escapeHtml(safeResult.summary.riskScore)}</p>
|
|
420
|
+
</div>
|
|
421
|
+
<p class="score-caption">${escapeHtml(riskCaption(safeResult.summary.riskScore))}</p>
|
|
422
|
+
</aside>
|
|
423
|
+
</header>
|
|
424
|
+
|
|
425
|
+
<section>
|
|
426
|
+
<h2>Severity Summary</h2>
|
|
427
|
+
<div class="severity-row">
|
|
428
|
+
${severityCard("critical", safeResult.summary.counts.critical)}
|
|
429
|
+
${severityCard("high", safeResult.summary.counts.high)}
|
|
430
|
+
${severityCard("medium", safeResult.summary.counts.medium)}
|
|
431
|
+
${severityCard("low", safeResult.summary.counts.low)}
|
|
432
|
+
</div>
|
|
433
|
+
</section>
|
|
434
|
+
|
|
435
|
+
<section>
|
|
436
|
+
<h2>Scanned Files</h2>
|
|
437
|
+
${renderScannedFiles(safeResult)}
|
|
438
|
+
</section>
|
|
439
|
+
|
|
440
|
+
<section>
|
|
441
|
+
<h2>MCP Server Inventory</h2>
|
|
442
|
+
${renderServerTable(servers, safeResult.metadata.cwd)}
|
|
443
|
+
</section>
|
|
444
|
+
|
|
445
|
+
<section>
|
|
446
|
+
<h2>Findings</h2>
|
|
447
|
+
${renderFindingsTable(findings)}
|
|
448
|
+
</section>
|
|
449
|
+
|
|
450
|
+
<section class="notes">
|
|
451
|
+
<h2>Review Notes</h2>
|
|
452
|
+
<ul>
|
|
453
|
+
<li>Secret-like values are redacted before rendering this report.</li>
|
|
454
|
+
<li>Review each server before granting access to files, shells, SaaS accounts, or production systems.</li>
|
|
455
|
+
<li>This report assists security review and does not guarantee that every issue was found.</li>
|
|
456
|
+
</ul>
|
|
457
|
+
</section>
|
|
458
|
+
</main>
|
|
459
|
+
</body>
|
|
460
|
+
</html>
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
|
|
108
464
|
function cell(value) {
|
|
109
465
|
return String(value).replaceAll("|", "\\|").replaceAll("\n", "<br>");
|
|
110
466
|
}
|
|
@@ -134,3 +490,108 @@ function sanitizeResult(result) {
|
|
|
134
490
|
summary: result.summary
|
|
135
491
|
};
|
|
136
492
|
}
|
|
493
|
+
|
|
494
|
+
function metric(label, value) {
|
|
495
|
+
return `<div class="metric"><strong>${escapeHtml(value)}</strong><span>${escapeHtml(label)}</span></div>`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function severityCard(severity, count) {
|
|
499
|
+
return `<div class="severity ${severity}"><b>${escapeHtml(count)}</b><span>${escapeHtml(severity)}</span></div>`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function renderScannedFiles(result) {
|
|
503
|
+
if (result.scannedFiles.length === 0) {
|
|
504
|
+
return `<p class="empty">No MCP config files were found.</p>`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const items = result.scannedFiles
|
|
508
|
+
.map((file) => `<tr><td><code>${escapeHtml(displayPath(file, result.metadata.cwd))}</code></td></tr>`)
|
|
509
|
+
.join("");
|
|
510
|
+
|
|
511
|
+
return `<div class="table-wrap"><table><thead><tr><th>Path</th></tr></thead><tbody>${items}</tbody></table></div>`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function renderServerTable(servers, cwd) {
|
|
515
|
+
if (servers.length === 0) {
|
|
516
|
+
return `<p class="empty">No MCP servers were found.</p>`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const rows = servers.map((server) => {
|
|
520
|
+
const env = kvList(server.env);
|
|
521
|
+
const headers = kvList(server.headers);
|
|
522
|
+
return `<tr>
|
|
523
|
+
<td><strong>${escapeHtml(server.name)}</strong><br><code>${escapeHtml(displayPath(server.configPath, cwd))}</code></td>
|
|
524
|
+
<td>${codeOrDash(server.command)}</td>
|
|
525
|
+
<td>${codeOrDash(server.args.join(" "))}</td>
|
|
526
|
+
<td>${codeOrDash(server.cwd)}</td>
|
|
527
|
+
<td>${codeOrDash(server.url)}</td>
|
|
528
|
+
<td>${env || "-"}</td>
|
|
529
|
+
<td>${headers || "-"}</td>
|
|
530
|
+
</tr>`;
|
|
531
|
+
}).join("");
|
|
532
|
+
|
|
533
|
+
return `<div class="table-wrap"><table>
|
|
534
|
+
<thead><tr><th>Server</th><th>Command</th><th>Args</th><th>CWD</th><th>URL</th><th>Env</th><th>Headers</th></tr></thead>
|
|
535
|
+
<tbody>${rows}</tbody>
|
|
536
|
+
</table></div>`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function renderFindingsTable(findings) {
|
|
540
|
+
if (findings.length === 0) {
|
|
541
|
+
return `<p class="empty">No findings. Keep reviewing new MCP servers and agent tools before adding them.</p>`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const rows = findings.map((finding) => `<tr>
|
|
545
|
+
<td><span class="pill ${escapeHtml(finding.severity)}">${escapeHtml(finding.severity)}</span></td>
|
|
546
|
+
<td><code>${escapeHtml(finding.id)}</code></td>
|
|
547
|
+
<td>${escapeHtml(finding.serverName)}</td>
|
|
548
|
+
<td>${escapeHtml(finding.title)}</td>
|
|
549
|
+
<td>${codeOrDash(finding.evidence)}</td>
|
|
550
|
+
<td>${escapeHtml(finding.recommendation)}</td>
|
|
551
|
+
</tr>`).join("");
|
|
552
|
+
|
|
553
|
+
return `<div class="table-wrap"><table>
|
|
554
|
+
<thead><tr><th>Severity</th><th>Rule</th><th>Server</th><th>Finding</th><th>Evidence</th><th>Recommendation</th></tr></thead>
|
|
555
|
+
<tbody>${rows}</tbody>
|
|
556
|
+
</table></div>`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function kvList(record) {
|
|
560
|
+
return Object.entries(record || {})
|
|
561
|
+
.map(([key, value]) => `<code>${escapeHtml(key)}=${escapeHtml(value)}</code>`)
|
|
562
|
+
.join("<br>");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function codeOrDash(value) {
|
|
566
|
+
if (!value) return "-";
|
|
567
|
+
return `<code>${escapeHtml(value)}</code>`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function formatDate(value) {
|
|
571
|
+
const date = new Date(value);
|
|
572
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
573
|
+
return `${date.toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function riskToneForScore(score) {
|
|
577
|
+
if (score >= 80) return "critical";
|
|
578
|
+
if (score >= 50) return "high";
|
|
579
|
+
if (score >= 20) return "medium";
|
|
580
|
+
return "low";
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function riskCaption(score) {
|
|
584
|
+
if (score >= 80) return "Critical review recommended before enabling these tools.";
|
|
585
|
+
if (score >= 50) return "High risk configuration; review before team use.";
|
|
586
|
+
if (score >= 20) return "Moderate risk; confirm the intended permission scope.";
|
|
587
|
+
return "Low risk based on the current rule set.";
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function escapeHtml(value) {
|
|
591
|
+
return String(value ?? "")
|
|
592
|
+
.replaceAll("&", "&")
|
|
593
|
+
.replaceAll("<", "<")
|
|
594
|
+
.replaceAll(">", ">")
|
|
595
|
+
.replaceAll('"', """)
|
|
596
|
+
.replaceAll("'", "'");
|
|
597
|
+
}
|