agent-mcp-guard 0.1.1 → 0.3.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 +24 -2
- package/action.yml +135 -0
- package/docs/business-playbook.md +2 -2
- package/docs/github-action.md +87 -0
- package/docs/roadmap.md +9 -11
- package/package.json +6 -2
- package/scripts/action-summary.js +65 -0
- package/src/cli.js +13 -5
- package/src/report.js +582 -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.3.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,33 @@ 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
|
+
|
|
45
|
+
Generate SARIF for GitHub code scanning:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
mcp-guard scan --format sarif --output mcp-guard.sarif
|
|
49
|
+
```
|
|
50
|
+
|
|
39
51
|
Use in CI:
|
|
40
52
|
|
|
41
53
|
```bash
|
|
42
54
|
mcp-guard scan --config .mcp.json --fail-on high
|
|
43
55
|
```
|
|
44
56
|
|
|
57
|
+
Use the GitHub Action:
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
- uses: ChaoYue0307/mcp-guard@v0.3.0
|
|
61
|
+
with:
|
|
62
|
+
fail-on: high
|
|
63
|
+
upload-sarif: "true"
|
|
64
|
+
```
|
|
65
|
+
|
|
45
66
|
## What It Finds
|
|
46
67
|
|
|
47
68
|
| Risk | Why it matters |
|
|
@@ -98,7 +119,7 @@ MCP configs often contain sensitive local paths, internal hostnames, tokens, and
|
|
|
98
119
|
- no config upload;
|
|
99
120
|
- no external API call;
|
|
100
121
|
- secret-like values redacted in reports;
|
|
101
|
-
- text, Markdown,
|
|
122
|
+
- text, Markdown, HTML, JSON, and SARIF output for local review, CI artifacts, and GitHub code scanning.
|
|
102
123
|
|
|
103
124
|
## Commercial Support
|
|
104
125
|
|
|
@@ -113,6 +134,7 @@ Service details: [docs/paid-audit.md](docs/paid-audit.md)
|
|
|
113
134
|
## Documentation
|
|
114
135
|
|
|
115
136
|
- [Rule reference](docs/rules.md)
|
|
137
|
+
- [GitHub Action](docs/github-action.md)
|
|
116
138
|
- [Privacy and security](docs/privacy-and-security.md)
|
|
117
139
|
- [Roadmap](docs/roadmap.md)
|
|
118
140
|
- [Business playbook](docs/business-playbook.md)
|
package/action.yml
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
upload-artifact:
|
|
23
|
+
description: Upload generated reports as a workflow artifact.
|
|
24
|
+
required: false
|
|
25
|
+
default: "true"
|
|
26
|
+
upload-sarif:
|
|
27
|
+
description: "Upload SARIF to GitHub code scanning. Requires security-events: write permission."
|
|
28
|
+
required: false
|
|
29
|
+
default: "false"
|
|
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
|
+
sarif-report:
|
|
46
|
+
description: Path to the generated SARIF report.
|
|
47
|
+
value: ${{ steps.reports.outputs.sarif-report }}
|
|
48
|
+
exit-code:
|
|
49
|
+
description: mcp-guard threshold exit code.
|
|
50
|
+
value: ${{ steps.reports.outputs.exit-code }}
|
|
51
|
+
|
|
52
|
+
runs:
|
|
53
|
+
using: composite
|
|
54
|
+
steps:
|
|
55
|
+
- name: Set up Node.js
|
|
56
|
+
uses: actions/setup-node@v4
|
|
57
|
+
with:
|
|
58
|
+
node-version: "20"
|
|
59
|
+
|
|
60
|
+
- name: Generate reports
|
|
61
|
+
id: reports
|
|
62
|
+
shell: bash
|
|
63
|
+
env:
|
|
64
|
+
MCP_GUARD_CONFIG: ${{ inputs.config }}
|
|
65
|
+
MCP_GUARD_FAIL_ON: ${{ inputs.fail-on }}
|
|
66
|
+
MCP_GUARD_OUTPUT_DIR: ${{ inputs.output-dir }}
|
|
67
|
+
run: |
|
|
68
|
+
set -euo pipefail
|
|
69
|
+
|
|
70
|
+
guard_bin="${GITHUB_ACTION_PATH}/bin/mcp-guard.js"
|
|
71
|
+
mkdir -p "${MCP_GUARD_OUTPUT_DIR}"
|
|
72
|
+
|
|
73
|
+
scan_args=()
|
|
74
|
+
if [ -n "${MCP_GUARD_CONFIG}" ]; then
|
|
75
|
+
scan_args+=(--config "${MCP_GUARD_CONFIG}")
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
markdown_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.md"
|
|
79
|
+
html_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.html"
|
|
80
|
+
json_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.json"
|
|
81
|
+
sarif_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard.sarif"
|
|
82
|
+
|
|
83
|
+
node "${guard_bin}" scan "${scan_args[@]}" --format markdown --output "${markdown_report}" --fail-on none
|
|
84
|
+
node "${guard_bin}" scan "${scan_args[@]}" --format html --output "${html_report}" --fail-on none
|
|
85
|
+
node "${guard_bin}" scan "${scan_args[@]}" --format json --output "${json_report}" --fail-on none
|
|
86
|
+
node "${guard_bin}" scan "${scan_args[@]}" --format sarif --output "${sarif_report}" --fail-on none
|
|
87
|
+
|
|
88
|
+
set +e
|
|
89
|
+
node "${guard_bin}" 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 "sarif-report=${sarif_report}"
|
|
98
|
+
echo "exit-code=${status}"
|
|
99
|
+
} >> "${GITHUB_OUTPUT}"
|
|
100
|
+
|
|
101
|
+
- name: Write job summary
|
|
102
|
+
if: ${{ always() && steps.reports.outputs.json-report != '' }}
|
|
103
|
+
shell: bash
|
|
104
|
+
env:
|
|
105
|
+
MCP_GUARD_FAIL_ON: ${{ inputs.fail-on }}
|
|
106
|
+
run: |
|
|
107
|
+
node "${GITHUB_ACTION_PATH}/scripts/action-summary.js" \
|
|
108
|
+
"${{ steps.reports.outputs.json-report }}" \
|
|
109
|
+
"${{ steps.reports.outputs.markdown-report }}" \
|
|
110
|
+
"${{ steps.reports.outputs.html-report }}" \
|
|
111
|
+
"${{ steps.reports.outputs.sarif-report }}" \
|
|
112
|
+
"${MCP_GUARD_FAIL_ON}" >> "${GITHUB_STEP_SUMMARY}"
|
|
113
|
+
|
|
114
|
+
- name: Upload report artifact
|
|
115
|
+
if: ${{ always() && inputs.upload-artifact == 'true' }}
|
|
116
|
+
uses: actions/upload-artifact@v4
|
|
117
|
+
with:
|
|
118
|
+
name: ${{ inputs.artifact-name }}
|
|
119
|
+
path: ${{ inputs.output-dir }}
|
|
120
|
+
|
|
121
|
+
- name: Upload SARIF to code scanning
|
|
122
|
+
if: ${{ always() && inputs.upload-sarif == 'true' && steps.reports.outputs.sarif-report != '' }}
|
|
123
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
124
|
+
with:
|
|
125
|
+
sarif_file: ${{ steps.reports.outputs.sarif-report }}
|
|
126
|
+
|
|
127
|
+
- name: Enforce severity threshold
|
|
128
|
+
shell: bash
|
|
129
|
+
env:
|
|
130
|
+
MCP_GUARD_EXIT_CODE: ${{ steps.reports.outputs.exit-code }}
|
|
131
|
+
run: |
|
|
132
|
+
if [ -z "${MCP_GUARD_EXIT_CODE}" ]; then
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
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, JSON, and SARIF 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,87 @@
|
|
|
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 runs the CLI from the pinned GitHub Action tag, generates Markdown, HTML, JSON, and SARIF reports, writes a job summary, uploads reports as an artifact, and 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.3.0
|
|
26
|
+
with:
|
|
27
|
+
fail-on: high
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Upload SARIF to GitHub Security
|
|
31
|
+
|
|
32
|
+
Enable SARIF upload when you want findings in the repository Security tab. The workflow needs `security-events: write`.
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
name: mcp-guard
|
|
36
|
+
|
|
37
|
+
on:
|
|
38
|
+
pull_request:
|
|
39
|
+
push:
|
|
40
|
+
branches: [main]
|
|
41
|
+
|
|
42
|
+
permissions:
|
|
43
|
+
contents: read
|
|
44
|
+
security-events: write
|
|
45
|
+
|
|
46
|
+
jobs:
|
|
47
|
+
scan:
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v4
|
|
51
|
+
- uses: ChaoYue0307/mcp-guard@v0.3.0
|
|
52
|
+
with:
|
|
53
|
+
config: .mcp.json
|
|
54
|
+
fail-on: high
|
|
55
|
+
upload-sarif: "true"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Report-Only Mode
|
|
59
|
+
|
|
60
|
+
Use `fail-on: none` when you want artifacts and summaries without blocking a pull request.
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
- uses: ChaoYue0307/mcp-guard@v0.3.0
|
|
64
|
+
with:
|
|
65
|
+
fail-on: none
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Inputs
|
|
69
|
+
|
|
70
|
+
| Input | Default | Description |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `config` | empty | Optional MCP config path. Empty scans default project and user config locations. |
|
|
73
|
+
| `fail-on` | `high` | Fails the job for `critical`, `high`, `medium`, or `low` findings. Use `none` for report-only mode. |
|
|
74
|
+
| `output-dir` | `mcp-guard-report` | Directory for generated reports. |
|
|
75
|
+
| `upload-artifact` | `true` | Uploads generated reports as a workflow artifact. |
|
|
76
|
+
| `upload-sarif` | `false` | Uploads SARIF to GitHub code scanning. Requires `security-events: write`. |
|
|
77
|
+
| `artifact-name` | `mcp-guard-report` | Name of the uploaded artifact. |
|
|
78
|
+
|
|
79
|
+
## Outputs
|
|
80
|
+
|
|
81
|
+
| Output | Description |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `markdown-report` | Path to the generated Markdown report. |
|
|
84
|
+
| `html-report` | Path to the generated HTML report. |
|
|
85
|
+
| `json-report` | Path to the generated JSON report. |
|
|
86
|
+
| `sarif-report` | Path to the generated SARIF report. |
|
|
87
|
+
| `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,
|
|
8
|
+
- Text, Markdown, HTML, redacted JSON, and SARIF 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 writes a job summary, uploads Markdown/HTML/JSON/SARIF artifacts, and can upload SARIF to GitHub code scanning.
|
|
11
12
|
|
|
12
13
|
## Next
|
|
13
14
|
|
|
14
|
-
1.
|
|
15
|
-
2.
|
|
16
|
-
3.
|
|
17
|
-
4.
|
|
15
|
+
1. More MCP client discovery paths.
|
|
16
|
+
2. Rule packs mapped to MCP security best practices.
|
|
17
|
+
3. Policy file for approved commands, packages, directories, and remote URLs.
|
|
18
|
+
4. Baseline mode: accept known findings and fail only on new risks.
|
|
18
19
|
5. `mcp-guard audit` mode for client-ready reports.
|
|
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.3.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/",
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"security",
|
|
33
33
|
"cli",
|
|
34
34
|
"scanner",
|
|
35
|
-
"devsecops"
|
|
35
|
+
"devsecops",
|
|
36
|
+
"sarif",
|
|
37
|
+
"github-actions"
|
|
36
38
|
],
|
|
37
39
|
"author": "",
|
|
38
40
|
"license": "Apache-2.0",
|
|
@@ -40,6 +42,8 @@
|
|
|
40
42
|
"bin",
|
|
41
43
|
"src",
|
|
42
44
|
"README.md",
|
|
45
|
+
"action.yml",
|
|
46
|
+
"scripts/action-summary.js",
|
|
43
47
|
"LICENSE",
|
|
44
48
|
"SECURITY.md",
|
|
45
49
|
"docs",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const [jsonReportPath, markdownReportPath, htmlReportPath, sarifReportPath, failOn] = process.argv.slice(2);
|
|
7
|
+
|
|
8
|
+
if (!jsonReportPath) {
|
|
9
|
+
process.stderr.write("Usage: action-summary.js <json-report> <markdown-report> <html-report> <sarif-report> <fail-on>\n");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const report = JSON.parse(fs.readFileSync(jsonReportPath, "utf8"));
|
|
14
|
+
const counts = report.summary.counts;
|
|
15
|
+
const topFindings = report.findings.slice(0, 8);
|
|
16
|
+
|
|
17
|
+
const lines = [];
|
|
18
|
+
lines.push("## mcp-guard scan");
|
|
19
|
+
lines.push("");
|
|
20
|
+
lines.push(`Risk score: **${report.summary.riskScore}**`);
|
|
21
|
+
lines.push("");
|
|
22
|
+
lines.push("| Severity | Count |");
|
|
23
|
+
lines.push("| --- | ---: |");
|
|
24
|
+
lines.push(`| Critical | ${counts.critical} |`);
|
|
25
|
+
lines.push(`| High | ${counts.high} |`);
|
|
26
|
+
lines.push(`| Medium | ${counts.medium} |`);
|
|
27
|
+
lines.push(`| Low | ${counts.low} |`);
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push(`Scanned files: **${report.summary.scannedFileCount}**`);
|
|
30
|
+
lines.push(`MCP servers: **${report.summary.serverCount}**`);
|
|
31
|
+
lines.push(`Findings: **${report.summary.findingCount}**`);
|
|
32
|
+
lines.push(`Fail threshold: **${failOn || "high"}**`);
|
|
33
|
+
lines.push("");
|
|
34
|
+
|
|
35
|
+
if (topFindings.length === 0) {
|
|
36
|
+
lines.push("No findings.");
|
|
37
|
+
} else {
|
|
38
|
+
lines.push("### Top findings");
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push("| Severity | Rule | Server | Finding |");
|
|
41
|
+
lines.push("| --- | --- | --- | --- |");
|
|
42
|
+
for (const finding of topFindings) {
|
|
43
|
+
lines.push(`| ${cell(finding.severity)} | ${cell(finding.id)} | ${cell(finding.serverName)} | ${cell(finding.title)} |`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push("### Reports");
|
|
49
|
+
lines.push("");
|
|
50
|
+
lines.push(`- Markdown: \`${relative(markdownReportPath)}\``);
|
|
51
|
+
lines.push(`- HTML: \`${relative(htmlReportPath)}\``);
|
|
52
|
+
lines.push(`- JSON: \`${relative(jsonReportPath)}\``);
|
|
53
|
+
lines.push(`- SARIF: \`${relative(sarifReportPath)}\``);
|
|
54
|
+
lines.push("");
|
|
55
|
+
|
|
56
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
57
|
+
|
|
58
|
+
function cell(value) {
|
|
59
|
+
return String(value ?? "").replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function relative(filePath) {
|
|
63
|
+
if (!filePath) return "";
|
|
64
|
+
return path.relative(process.cwd(), path.resolve(filePath)) || ".";
|
|
65
|
+
}
|
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, generateSarifReport, generateTextReport } from "./report.js";
|
|
5
5
|
import { compareSeverity, severityRank } from "./severity.js";
|
|
6
6
|
|
|
7
|
-
const VERSION = "0.
|
|
7
|
+
const VERSION = "0.3.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", "sarif"].includes(options.format)) {
|
|
84
|
+
throw new Error("--format must be one of: text, markdown, json, html, sarif");
|
|
85
85
|
}
|
|
86
86
|
} else if (arg === "--fail-on") {
|
|
87
87
|
options.failOn = readValue(args, index, arg);
|
|
@@ -121,6 +121,12 @@ 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
|
+
}
|
|
127
|
+
if (format === "sarif") {
|
|
128
|
+
return `${generateSarifReport(result)}\n`;
|
|
129
|
+
}
|
|
124
130
|
return generateTextReport(result);
|
|
125
131
|
}
|
|
126
132
|
|
|
@@ -142,7 +148,7 @@ Usage:
|
|
|
142
148
|
Scan options:
|
|
143
149
|
-c, --config <path> Scan a specific MCP config file. Can be repeated.
|
|
144
150
|
-o, --output <path> Write report to a file.
|
|
145
|
-
-f, --format <format> text, markdown, or
|
|
151
|
+
-f, --format <format> text, markdown, json, html, or sarif. Default: text.
|
|
146
152
|
--fail-on <severity> Exit 2 when finding severity is at least threshold.
|
|
147
153
|
critical, high, medium, low, none. Default: none.
|
|
148
154
|
--cwd <path> Working directory for project config discovery.
|
|
@@ -151,6 +157,8 @@ Scan options:
|
|
|
151
157
|
Examples:
|
|
152
158
|
mcp-guard scan
|
|
153
159
|
mcp-guard scan --format markdown --output mcp-guard-report.md
|
|
160
|
+
mcp-guard scan --format html --output mcp-guard-report.html
|
|
161
|
+
mcp-guard scan --format sarif --output mcp-guard.sarif
|
|
154
162
|
mcp-guard scan --config .mcp.json --fail-on high
|
|
155
163
|
`;
|
|
156
164
|
}
|
package/src/report.js
CHANGED
|
@@ -105,6 +105,396 @@ export function generateJsonReport(result) {
|
|
|
105
105
|
return JSON.stringify(sanitizeResult(result), null, 2);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export function generateSarifReport(result) {
|
|
109
|
+
const rules = buildSarifRules(result.findings);
|
|
110
|
+
const sarif = {
|
|
111
|
+
version: "2.1.0",
|
|
112
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
113
|
+
runs: [
|
|
114
|
+
{
|
|
115
|
+
tool: {
|
|
116
|
+
driver: {
|
|
117
|
+
name: "mcp-guard",
|
|
118
|
+
informationUri: "https://github.com/ChaoYue0307/mcp-guard",
|
|
119
|
+
semanticVersion: result.metadata.toolVersion,
|
|
120
|
+
rules
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
automationDetails: {
|
|
124
|
+
id: "mcp-guard/"
|
|
125
|
+
},
|
|
126
|
+
invocations: [
|
|
127
|
+
{
|
|
128
|
+
executionSuccessful: true,
|
|
129
|
+
workingDirectory: {
|
|
130
|
+
uri: uriFromPath(result.metadata.cwd, result.metadata.cwd)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
results: result.findings.map((finding) => sarifResult(finding, result.metadata.cwd))
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return JSON.stringify(sarif, null, 2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function generateHtmlReport(result) {
|
|
143
|
+
const safeResult = sanitizeResult(result);
|
|
144
|
+
const riskTone = riskToneForScore(safeResult.summary.riskScore);
|
|
145
|
+
const findings = safeResult.findings;
|
|
146
|
+
const servers = safeResult.servers;
|
|
147
|
+
|
|
148
|
+
return `<!doctype html>
|
|
149
|
+
<html lang="en">
|
|
150
|
+
<head>
|
|
151
|
+
<meta charset="utf-8">
|
|
152
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
153
|
+
<title>mcp-guard Scan Report</title>
|
|
154
|
+
<style>
|
|
155
|
+
:root {
|
|
156
|
+
color-scheme: light;
|
|
157
|
+
--bg: #f8fafc;
|
|
158
|
+
--panel: #ffffff;
|
|
159
|
+
--ink: #111827;
|
|
160
|
+
--muted: #5b6575;
|
|
161
|
+
--line: #d9e2ec;
|
|
162
|
+
--soft: #eef4f7;
|
|
163
|
+
--critical: #b91c1c;
|
|
164
|
+
--high: #c2410c;
|
|
165
|
+
--medium: #a16207;
|
|
166
|
+
--low: #0f766e;
|
|
167
|
+
--info: #1d4ed8;
|
|
168
|
+
--shadow: 0 20px 55px rgba(15, 23, 42, 0.08);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
* { box-sizing: border-box; }
|
|
172
|
+
|
|
173
|
+
body {
|
|
174
|
+
margin: 0;
|
|
175
|
+
background: var(--bg);
|
|
176
|
+
color: var(--ink);
|
|
177
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
178
|
+
line-height: 1.5;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main {
|
|
182
|
+
width: min(1120px, calc(100% - 32px));
|
|
183
|
+
margin: 0 auto;
|
|
184
|
+
padding: 28px 0 48px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.hero {
|
|
188
|
+
background: #ffffff;
|
|
189
|
+
border: 1px solid var(--line);
|
|
190
|
+
border-radius: 8px;
|
|
191
|
+
box-shadow: var(--shadow);
|
|
192
|
+
padding: 28px;
|
|
193
|
+
display: grid;
|
|
194
|
+
grid-template-columns: minmax(0, 1fr) 240px;
|
|
195
|
+
gap: 24px;
|
|
196
|
+
align-items: stretch;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.eyebrow {
|
|
200
|
+
margin: 0 0 8px;
|
|
201
|
+
color: var(--info);
|
|
202
|
+
font-size: 13px;
|
|
203
|
+
font-weight: 700;
|
|
204
|
+
letter-spacing: 0;
|
|
205
|
+
text-transform: uppercase;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
h1, h2 {
|
|
209
|
+
letter-spacing: 0;
|
|
210
|
+
line-height: 1.08;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
h1 {
|
|
214
|
+
margin: 0;
|
|
215
|
+
font-size: 34px;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
h2 {
|
|
219
|
+
margin: 0 0 14px;
|
|
220
|
+
font-size: 21px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.lead {
|
|
224
|
+
max-width: 720px;
|
|
225
|
+
margin: 12px 0 0;
|
|
226
|
+
color: var(--muted);
|
|
227
|
+
font-size: 16px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.scorecard {
|
|
231
|
+
border-radius: 8px;
|
|
232
|
+
border: 1px solid var(--line);
|
|
233
|
+
background: var(--soft);
|
|
234
|
+
padding: 18px;
|
|
235
|
+
min-height: 176px;
|
|
236
|
+
display: flex;
|
|
237
|
+
flex-direction: column;
|
|
238
|
+
justify-content: space-between;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.score-label {
|
|
242
|
+
margin: 0;
|
|
243
|
+
color: var(--muted);
|
|
244
|
+
font-size: 13px;
|
|
245
|
+
font-weight: 700;
|
|
246
|
+
text-transform: uppercase;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.score-value {
|
|
250
|
+
margin: 8px 0;
|
|
251
|
+
font-size: 58px;
|
|
252
|
+
font-weight: 800;
|
|
253
|
+
line-height: 1;
|
|
254
|
+
color: var(--${riskTone});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.score-caption {
|
|
258
|
+
margin: 0;
|
|
259
|
+
color: var(--muted);
|
|
260
|
+
font-size: 14px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.grid {
|
|
264
|
+
display: grid;
|
|
265
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
266
|
+
gap: 12px;
|
|
267
|
+
margin: 18px 0 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.metric {
|
|
271
|
+
background: var(--panel);
|
|
272
|
+
border: 1px solid var(--line);
|
|
273
|
+
border-radius: 8px;
|
|
274
|
+
padding: 14px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.metric strong {
|
|
278
|
+
display: block;
|
|
279
|
+
font-size: 24px;
|
|
280
|
+
line-height: 1.1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.metric span {
|
|
284
|
+
display: block;
|
|
285
|
+
margin-top: 6px;
|
|
286
|
+
color: var(--muted);
|
|
287
|
+
font-size: 13px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
section {
|
|
291
|
+
margin-top: 22px;
|
|
292
|
+
background: var(--panel);
|
|
293
|
+
border: 1px solid var(--line);
|
|
294
|
+
border-radius: 8px;
|
|
295
|
+
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.04);
|
|
296
|
+
padding: 22px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.severity-row {
|
|
300
|
+
display: grid;
|
|
301
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
302
|
+
gap: 10px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.severity {
|
|
306
|
+
border: 1px solid var(--line);
|
|
307
|
+
border-radius: 8px;
|
|
308
|
+
padding: 12px;
|
|
309
|
+
min-height: 78px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.severity b {
|
|
313
|
+
display: block;
|
|
314
|
+
font-size: 22px;
|
|
315
|
+
line-height: 1.1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.severity span {
|
|
319
|
+
display: block;
|
|
320
|
+
margin-top: 6px;
|
|
321
|
+
color: var(--muted);
|
|
322
|
+
font-size: 13px;
|
|
323
|
+
text-transform: capitalize;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.critical { color: var(--critical); }
|
|
327
|
+
.high { color: var(--high); }
|
|
328
|
+
.medium { color: var(--medium); }
|
|
329
|
+
.low { color: var(--low); }
|
|
330
|
+
|
|
331
|
+
.table-wrap {
|
|
332
|
+
width: 100%;
|
|
333
|
+
overflow-x: auto;
|
|
334
|
+
border: 1px solid var(--line);
|
|
335
|
+
border-radius: 8px;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
table {
|
|
339
|
+
width: 100%;
|
|
340
|
+
min-width: 760px;
|
|
341
|
+
border-collapse: collapse;
|
|
342
|
+
background: var(--panel);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
th, td {
|
|
346
|
+
padding: 12px 14px;
|
|
347
|
+
border-bottom: 1px solid var(--line);
|
|
348
|
+
text-align: left;
|
|
349
|
+
vertical-align: top;
|
|
350
|
+
font-size: 14px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
th {
|
|
354
|
+
color: #374151;
|
|
355
|
+
background: #f1f5f9;
|
|
356
|
+
font-size: 12px;
|
|
357
|
+
text-transform: uppercase;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
tr:last-child td { border-bottom: 0; }
|
|
361
|
+
|
|
362
|
+
code {
|
|
363
|
+
padding: 2px 5px;
|
|
364
|
+
border-radius: 5px;
|
|
365
|
+
background: #eef2f7;
|
|
366
|
+
color: #111827;
|
|
367
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
368
|
+
font-size: 0.92em;
|
|
369
|
+
white-space: normal;
|
|
370
|
+
overflow-wrap: anywhere;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.pill {
|
|
374
|
+
display: inline-flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
min-height: 24px;
|
|
377
|
+
padding: 3px 8px;
|
|
378
|
+
border-radius: 999px;
|
|
379
|
+
background: #f1f5f9;
|
|
380
|
+
font-size: 12px;
|
|
381
|
+
font-weight: 700;
|
|
382
|
+
text-transform: uppercase;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.pill.critical { background: #fee2e2; }
|
|
386
|
+
.pill.high { background: #ffedd5; }
|
|
387
|
+
.pill.medium { background: #fef3c7; }
|
|
388
|
+
.pill.low { background: #ccfbf1; }
|
|
389
|
+
|
|
390
|
+
.empty {
|
|
391
|
+
margin: 0;
|
|
392
|
+
color: var(--muted);
|
|
393
|
+
border: 1px dashed var(--line);
|
|
394
|
+
border-radius: 8px;
|
|
395
|
+
padding: 16px;
|
|
396
|
+
background: #fbfdff;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.notes {
|
|
400
|
+
color: var(--muted);
|
|
401
|
+
font-size: 14px;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.notes ul {
|
|
405
|
+
margin: 10px 0 0;
|
|
406
|
+
padding-left: 18px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
@media (max-width: 780px) {
|
|
410
|
+
main {
|
|
411
|
+
width: min(100% - 20px, 1120px);
|
|
412
|
+
padding-top: 10px;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.hero {
|
|
416
|
+
grid-template-columns: 1fr;
|
|
417
|
+
padding: 20px;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
h1 { font-size: 28px; }
|
|
421
|
+
|
|
422
|
+
.grid,
|
|
423
|
+
.severity-row {
|
|
424
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
@media (max-width: 460px) {
|
|
429
|
+
.grid,
|
|
430
|
+
.severity-row {
|
|
431
|
+
grid-template-columns: 1fr;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
</style>
|
|
435
|
+
</head>
|
|
436
|
+
<body>
|
|
437
|
+
<main>
|
|
438
|
+
<header class="hero">
|
|
439
|
+
<div>
|
|
440
|
+
<p class="eyebrow">mcp-guard scan report</p>
|
|
441
|
+
<h1>AI agent tool risk review</h1>
|
|
442
|
+
<p class="lead">Local-first review of MCP server configuration, startup commands, remote endpoints, filesystem scope, and secret-like values.</p>
|
|
443
|
+
<div class="grid">
|
|
444
|
+
${metric("Scanned files", safeResult.summary.scannedFileCount)}
|
|
445
|
+
${metric("MCP servers", safeResult.summary.serverCount)}
|
|
446
|
+
${metric("Findings", safeResult.summary.findingCount)}
|
|
447
|
+
${metric("Generated", formatDate(safeResult.metadata.generatedAt))}
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
<aside class="scorecard" aria-label="Risk score">
|
|
451
|
+
<div>
|
|
452
|
+
<p class="score-label">Risk score</p>
|
|
453
|
+
<p class="score-value">${escapeHtml(safeResult.summary.riskScore)}</p>
|
|
454
|
+
</div>
|
|
455
|
+
<p class="score-caption">${escapeHtml(riskCaption(safeResult.summary.riskScore))}</p>
|
|
456
|
+
</aside>
|
|
457
|
+
</header>
|
|
458
|
+
|
|
459
|
+
<section>
|
|
460
|
+
<h2>Severity Summary</h2>
|
|
461
|
+
<div class="severity-row">
|
|
462
|
+
${severityCard("critical", safeResult.summary.counts.critical)}
|
|
463
|
+
${severityCard("high", safeResult.summary.counts.high)}
|
|
464
|
+
${severityCard("medium", safeResult.summary.counts.medium)}
|
|
465
|
+
${severityCard("low", safeResult.summary.counts.low)}
|
|
466
|
+
</div>
|
|
467
|
+
</section>
|
|
468
|
+
|
|
469
|
+
<section>
|
|
470
|
+
<h2>Scanned Files</h2>
|
|
471
|
+
${renderScannedFiles(safeResult)}
|
|
472
|
+
</section>
|
|
473
|
+
|
|
474
|
+
<section>
|
|
475
|
+
<h2>MCP Server Inventory</h2>
|
|
476
|
+
${renderServerTable(servers, safeResult.metadata.cwd)}
|
|
477
|
+
</section>
|
|
478
|
+
|
|
479
|
+
<section>
|
|
480
|
+
<h2>Findings</h2>
|
|
481
|
+
${renderFindingsTable(findings)}
|
|
482
|
+
</section>
|
|
483
|
+
|
|
484
|
+
<section class="notes">
|
|
485
|
+
<h2>Review Notes</h2>
|
|
486
|
+
<ul>
|
|
487
|
+
<li>Secret-like values are redacted before rendering this report.</li>
|
|
488
|
+
<li>Review each server before granting access to files, shells, SaaS accounts, or production systems.</li>
|
|
489
|
+
<li>This report assists security review and does not guarantee that every issue was found.</li>
|
|
490
|
+
</ul>
|
|
491
|
+
</section>
|
|
492
|
+
</main>
|
|
493
|
+
</body>
|
|
494
|
+
</html>
|
|
495
|
+
`;
|
|
496
|
+
}
|
|
497
|
+
|
|
108
498
|
function cell(value) {
|
|
109
499
|
return String(value).replaceAll("|", "\\|").replaceAll("\n", "<br>");
|
|
110
500
|
}
|
|
@@ -134,3 +524,195 @@ function sanitizeResult(result) {
|
|
|
134
524
|
summary: result.summary
|
|
135
525
|
};
|
|
136
526
|
}
|
|
527
|
+
|
|
528
|
+
function buildSarifRules(findings) {
|
|
529
|
+
const unique = new Map();
|
|
530
|
+
for (const finding of findings) {
|
|
531
|
+
if (unique.has(finding.id)) continue;
|
|
532
|
+
unique.set(finding.id, {
|
|
533
|
+
id: finding.id,
|
|
534
|
+
name: finding.id,
|
|
535
|
+
shortDescription: {
|
|
536
|
+
text: finding.title
|
|
537
|
+
},
|
|
538
|
+
fullDescription: {
|
|
539
|
+
text: finding.title
|
|
540
|
+
},
|
|
541
|
+
help: {
|
|
542
|
+
text: finding.recommendation,
|
|
543
|
+
markdown: finding.recommendation
|
|
544
|
+
},
|
|
545
|
+
defaultConfiguration: {
|
|
546
|
+
level: sarifLevel(finding.severity)
|
|
547
|
+
},
|
|
548
|
+
properties: {
|
|
549
|
+
severity: finding.severity,
|
|
550
|
+
tags: ["mcp", "ai-agent", "security"]
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return [...unique.values()];
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function sarifResult(finding, cwd) {
|
|
558
|
+
return {
|
|
559
|
+
ruleId: finding.id,
|
|
560
|
+
level: sarifLevel(finding.severity),
|
|
561
|
+
message: {
|
|
562
|
+
text: `${finding.title}. ${finding.evidence} Fix: ${finding.recommendation}`
|
|
563
|
+
},
|
|
564
|
+
locations: [
|
|
565
|
+
{
|
|
566
|
+
physicalLocation: {
|
|
567
|
+
artifactLocation: {
|
|
568
|
+
uri: uriFromPath(finding.configPath, cwd)
|
|
569
|
+
},
|
|
570
|
+
region: {
|
|
571
|
+
startLine: 1,
|
|
572
|
+
startColumn: 1
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
logicalLocations: [
|
|
576
|
+
{
|
|
577
|
+
name: finding.serverName,
|
|
578
|
+
kind: "object"
|
|
579
|
+
}
|
|
580
|
+
]
|
|
581
|
+
}
|
|
582
|
+
],
|
|
583
|
+
partialFingerprints: {
|
|
584
|
+
"mcp-guard/rule-server-evidence": fingerprint(`${finding.id}:${finding.serverName}:${finding.evidence}`)
|
|
585
|
+
},
|
|
586
|
+
properties: {
|
|
587
|
+
severity: finding.severity,
|
|
588
|
+
serverName: finding.serverName,
|
|
589
|
+
evidence: finding.evidence,
|
|
590
|
+
recommendation: finding.recommendation
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function sarifLevel(severity) {
|
|
596
|
+
if (severity === "critical" || severity === "high") return "error";
|
|
597
|
+
if (severity === "medium") return "warning";
|
|
598
|
+
return "note";
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function uriFromPath(filePath, cwd) {
|
|
602
|
+
const display = displayPath(filePath, cwd) || ".";
|
|
603
|
+
return display.split("/").map(encodeURIComponent).join("/");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function fingerprint(value) {
|
|
607
|
+
let hash = 2166136261;
|
|
608
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
609
|
+
hash ^= value.charCodeAt(index);
|
|
610
|
+
hash = Math.imul(hash, 16777619);
|
|
611
|
+
}
|
|
612
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function metric(label, value) {
|
|
616
|
+
return `<div class="metric"><strong>${escapeHtml(value)}</strong><span>${escapeHtml(label)}</span></div>`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function severityCard(severity, count) {
|
|
620
|
+
return `<div class="severity ${severity}"><b>${escapeHtml(count)}</b><span>${escapeHtml(severity)}</span></div>`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function renderScannedFiles(result) {
|
|
624
|
+
if (result.scannedFiles.length === 0) {
|
|
625
|
+
return `<p class="empty">No MCP config files were found.</p>`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const items = result.scannedFiles
|
|
629
|
+
.map((file) => `<tr><td><code>${escapeHtml(displayPath(file, result.metadata.cwd))}</code></td></tr>`)
|
|
630
|
+
.join("");
|
|
631
|
+
|
|
632
|
+
return `<div class="table-wrap"><table><thead><tr><th>Path</th></tr></thead><tbody>${items}</tbody></table></div>`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function renderServerTable(servers, cwd) {
|
|
636
|
+
if (servers.length === 0) {
|
|
637
|
+
return `<p class="empty">No MCP servers were found.</p>`;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const rows = servers.map((server) => {
|
|
641
|
+
const env = kvList(server.env);
|
|
642
|
+
const headers = kvList(server.headers);
|
|
643
|
+
return `<tr>
|
|
644
|
+
<td><strong>${escapeHtml(server.name)}</strong><br><code>${escapeHtml(displayPath(server.configPath, cwd))}</code></td>
|
|
645
|
+
<td>${codeOrDash(server.command)}</td>
|
|
646
|
+
<td>${codeOrDash(server.args.join(" "))}</td>
|
|
647
|
+
<td>${codeOrDash(server.cwd)}</td>
|
|
648
|
+
<td>${codeOrDash(server.url)}</td>
|
|
649
|
+
<td>${env || "-"}</td>
|
|
650
|
+
<td>${headers || "-"}</td>
|
|
651
|
+
</tr>`;
|
|
652
|
+
}).join("");
|
|
653
|
+
|
|
654
|
+
return `<div class="table-wrap"><table>
|
|
655
|
+
<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>
|
|
656
|
+
<tbody>${rows}</tbody>
|
|
657
|
+
</table></div>`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function renderFindingsTable(findings) {
|
|
661
|
+
if (findings.length === 0) {
|
|
662
|
+
return `<p class="empty">No findings. Keep reviewing new MCP servers and agent tools before adding them.</p>`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const rows = findings.map((finding) => `<tr>
|
|
666
|
+
<td><span class="pill ${escapeHtml(finding.severity)}">${escapeHtml(finding.severity)}</span></td>
|
|
667
|
+
<td><code>${escapeHtml(finding.id)}</code></td>
|
|
668
|
+
<td>${escapeHtml(finding.serverName)}</td>
|
|
669
|
+
<td>${escapeHtml(finding.title)}</td>
|
|
670
|
+
<td>${codeOrDash(finding.evidence)}</td>
|
|
671
|
+
<td>${escapeHtml(finding.recommendation)}</td>
|
|
672
|
+
</tr>`).join("");
|
|
673
|
+
|
|
674
|
+
return `<div class="table-wrap"><table>
|
|
675
|
+
<thead><tr><th>Severity</th><th>Rule</th><th>Server</th><th>Finding</th><th>Evidence</th><th>Recommendation</th></tr></thead>
|
|
676
|
+
<tbody>${rows}</tbody>
|
|
677
|
+
</table></div>`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function kvList(record) {
|
|
681
|
+
return Object.entries(record || {})
|
|
682
|
+
.map(([key, value]) => `<code>${escapeHtml(key)}=${escapeHtml(value)}</code>`)
|
|
683
|
+
.join("<br>");
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function codeOrDash(value) {
|
|
687
|
+
if (!value) return "-";
|
|
688
|
+
return `<code>${escapeHtml(value)}</code>`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function formatDate(value) {
|
|
692
|
+
const date = new Date(value);
|
|
693
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
694
|
+
return `${date.toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function riskToneForScore(score) {
|
|
698
|
+
if (score >= 80) return "critical";
|
|
699
|
+
if (score >= 50) return "high";
|
|
700
|
+
if (score >= 20) return "medium";
|
|
701
|
+
return "low";
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function riskCaption(score) {
|
|
705
|
+
if (score >= 80) return "Critical review recommended before enabling these tools.";
|
|
706
|
+
if (score >= 50) return "High risk configuration; review before team use.";
|
|
707
|
+
if (score >= 20) return "Moderate risk; confirm the intended permission scope.";
|
|
708
|
+
return "Low risk based on the current rule set.";
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function escapeHtml(value) {
|
|
712
|
+
return String(value ?? "")
|
|
713
|
+
.replaceAll("&", "&")
|
|
714
|
+
.replaceAll("<", "<")
|
|
715
|
+
.replaceAll(">", ">")
|
|
716
|
+
.replaceAll('"', """)
|
|
717
|
+
.replaceAll("'", "'");
|
|
718
|
+
}
|