agent-mcp-guard 0.2.0 → 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 +10 -3
- package/action.yml +39 -15
- package/docs/business-playbook.md +1 -1
- package/docs/github-action.md +32 -13
- package/docs/roadmap.md +5 -5
- package/package.json +5 -2
- package/scripts/action-summary.js +65 -0
- package/src/cli.js +9 -5
- package/src/report.js +121 -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
|
|
@@ -42,6 +42,12 @@ Generate an HTML report:
|
|
|
42
42
|
mcp-guard scan --format html --output mcp-guard-report.html
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
Generate SARIF for GitHub code scanning:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
mcp-guard scan --format sarif --output mcp-guard.sarif
|
|
49
|
+
```
|
|
50
|
+
|
|
45
51
|
Use in CI:
|
|
46
52
|
|
|
47
53
|
```bash
|
|
@@ -51,9 +57,10 @@ mcp-guard scan --config .mcp.json --fail-on high
|
|
|
51
57
|
Use the GitHub Action:
|
|
52
58
|
|
|
53
59
|
```yaml
|
|
54
|
-
- uses: ChaoYue0307/mcp-guard@v0.
|
|
60
|
+
- uses: ChaoYue0307/mcp-guard@v0.3.0
|
|
55
61
|
with:
|
|
56
62
|
fail-on: high
|
|
63
|
+
upload-sarif: "true"
|
|
57
64
|
```
|
|
58
65
|
|
|
59
66
|
## What It Finds
|
|
@@ -112,7 +119,7 @@ MCP configs often contain sensitive local paths, internal hostnames, tokens, and
|
|
|
112
119
|
- no config upload;
|
|
113
120
|
- no external API call;
|
|
114
121
|
- secret-like values redacted in reports;
|
|
115
|
-
- text, Markdown, HTML, and
|
|
122
|
+
- text, Markdown, HTML, JSON, and SARIF output for local review, CI artifacts, and GitHub code scanning.
|
|
116
123
|
|
|
117
124
|
## Commercial Support
|
|
118
125
|
|
package/action.yml
CHANGED
|
@@ -19,14 +19,14 @@ inputs:
|
|
|
19
19
|
description: Directory where reports will be written.
|
|
20
20
|
required: false
|
|
21
21
|
default: mcp-guard-report
|
|
22
|
-
package-version:
|
|
23
|
-
description: npm package version to install.
|
|
24
|
-
required: false
|
|
25
|
-
default: latest
|
|
26
22
|
upload-artifact:
|
|
27
23
|
description: Upload generated reports as a workflow artifact.
|
|
28
24
|
required: false
|
|
29
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
30
|
artifact-name:
|
|
31
31
|
description: Artifact name for generated reports.
|
|
32
32
|
required: false
|
|
@@ -42,6 +42,9 @@ outputs:
|
|
|
42
42
|
json-report:
|
|
43
43
|
description: Path to the generated JSON report.
|
|
44
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 }}
|
|
45
48
|
exit-code:
|
|
46
49
|
description: mcp-guard threshold exit code.
|
|
47
50
|
value: ${{ steps.reports.outputs.exit-code }}
|
|
@@ -54,12 +57,6 @@ runs:
|
|
|
54
57
|
with:
|
|
55
58
|
node-version: "20"
|
|
56
59
|
|
|
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
60
|
- name: Generate reports
|
|
64
61
|
id: reports
|
|
65
62
|
shell: bash
|
|
@@ -70,6 +67,7 @@ runs:
|
|
|
70
67
|
run: |
|
|
71
68
|
set -euo pipefail
|
|
72
69
|
|
|
70
|
+
guard_bin="${GITHUB_ACTION_PATH}/bin/mcp-guard.js"
|
|
73
71
|
mkdir -p "${MCP_GUARD_OUTPUT_DIR}"
|
|
74
72
|
|
|
75
73
|
scan_args=()
|
|
@@ -80,13 +78,15 @@ runs:
|
|
|
80
78
|
markdown_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.md"
|
|
81
79
|
html_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.html"
|
|
82
80
|
json_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.json"
|
|
81
|
+
sarif_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard.sarif"
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
87
|
|
|
88
88
|
set +e
|
|
89
|
-
|
|
89
|
+
node "${guard_bin}" scan "${scan_args[@]}" --fail-on "${MCP_GUARD_FAIL_ON}"
|
|
90
90
|
status="$?"
|
|
91
91
|
set -e
|
|
92
92
|
|
|
@@ -94,9 +94,23 @@ runs:
|
|
|
94
94
|
echo "markdown-report=${markdown_report}"
|
|
95
95
|
echo "html-report=${html_report}"
|
|
96
96
|
echo "json-report=${json_report}"
|
|
97
|
+
echo "sarif-report=${sarif_report}"
|
|
97
98
|
echo "exit-code=${status}"
|
|
98
99
|
} >> "${GITHUB_OUTPUT}"
|
|
99
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
|
+
|
|
100
114
|
- name: Upload report artifact
|
|
101
115
|
if: ${{ always() && inputs.upload-artifact == 'true' }}
|
|
102
116
|
uses: actions/upload-artifact@v4
|
|
@@ -104,8 +118,18 @@ runs:
|
|
|
104
118
|
name: ${{ inputs.artifact-name }}
|
|
105
119
|
path: ${{ inputs.output-dir }}
|
|
106
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
|
+
|
|
107
127
|
- name: Enforce severity threshold
|
|
108
128
|
shell: bash
|
|
109
129
|
env:
|
|
110
130
|
MCP_GUARD_EXIT_CODE: ${{ steps.reports.outputs.exit-code }}
|
|
111
|
-
run:
|
|
131
|
+
run: |
|
|
132
|
+
if [ -z "${MCP_GUARD_EXIT_CODE}" ]; then
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
exit "${MCP_GUARD_EXIT_CODE}"
|
|
@@ -13,7 +13,7 @@ AI Agent/MCP Security Audit.
|
|
|
13
13
|
Deliverables:
|
|
14
14
|
|
|
15
15
|
- MCP server inventory;
|
|
16
|
-
- `mcp-guard` Markdown, HTML, and
|
|
16
|
+
- `mcp-guard` Markdown, HTML, JSON, and SARIF scan reports;
|
|
17
17
|
- manual review of high-risk findings;
|
|
18
18
|
- prioritized remediation plan;
|
|
19
19
|
- optional GitHub Action setup for continuous scans;
|
package/docs/github-action.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use the `mcp-guard` action to scan MCP and AI agent tool configuration in pull requests and CI.
|
|
4
4
|
|
|
5
|
-
The action
|
|
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
6
|
|
|
7
7
|
## Basic Workflow
|
|
8
8
|
|
|
@@ -22,29 +22,47 @@ jobs:
|
|
|
22
22
|
runs-on: ubuntu-latest
|
|
23
23
|
steps:
|
|
24
24
|
- uses: actions/checkout@v4
|
|
25
|
-
- uses: ChaoYue0307/mcp-guard@v0.
|
|
25
|
+
- uses: ChaoYue0307/mcp-guard@v0.3.0
|
|
26
26
|
with:
|
|
27
27
|
fail-on: high
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
##
|
|
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`.
|
|
31
33
|
|
|
32
34
|
```yaml
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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"
|
|
37
56
|
```
|
|
38
57
|
|
|
39
|
-
##
|
|
58
|
+
## Report-Only Mode
|
|
40
59
|
|
|
41
|
-
|
|
60
|
+
Use `fail-on: none` when you want artifacts and summaries without blocking a pull request.
|
|
42
61
|
|
|
43
62
|
```yaml
|
|
44
|
-
- uses: ChaoYue0307/mcp-guard@v0.
|
|
63
|
+
- uses: ChaoYue0307/mcp-guard@v0.3.0
|
|
45
64
|
with:
|
|
46
|
-
|
|
47
|
-
fail-on: high
|
|
65
|
+
fail-on: none
|
|
48
66
|
```
|
|
49
67
|
|
|
50
68
|
## Inputs
|
|
@@ -54,8 +72,8 @@ The action defaults to `agent-mcp-guard@latest`. Pin it when you want determinis
|
|
|
54
72
|
| `config` | empty | Optional MCP config path. Empty scans default project and user config locations. |
|
|
55
73
|
| `fail-on` | `high` | Fails the job for `critical`, `high`, `medium`, or `low` findings. Use `none` for report-only mode. |
|
|
56
74
|
| `output-dir` | `mcp-guard-report` | Directory for generated reports. |
|
|
57
|
-
| `package-version` | `latest` | npm package version to install. |
|
|
58
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`. |
|
|
59
77
|
| `artifact-name` | `mcp-guard-report` | Name of the uploaded artifact. |
|
|
60
78
|
|
|
61
79
|
## Outputs
|
|
@@ -65,4 +83,5 @@ The action defaults to `agent-mcp-guard@latest`. Pin it when you want determinis
|
|
|
65
83
|
| `markdown-report` | Path to the generated Markdown report. |
|
|
66
84
|
| `html-report` | Path to the generated HTML report. |
|
|
67
85
|
| `json-report` | Path to the generated JSON report. |
|
|
86
|
+
| `sarif-report` | Path to the generated SARIF report. |
|
|
68
87
|
| `exit-code` | `0` when below threshold, `2` when findings met the threshold. |
|
package/docs/roadmap.md
CHANGED
|
@@ -5,18 +5,18 @@
|
|
|
5
5
|
## Now
|
|
6
6
|
|
|
7
7
|
- CLI config scanning.
|
|
8
|
-
- Text, Markdown, HTML,
|
|
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 uploads Markdown
|
|
11
|
+
- GitHub Action wrapper that writes a job summary, uploads Markdown/HTML/JSON/SARIF artifacts, and can upload SARIF to GitHub code scanning.
|
|
12
12
|
|
|
13
13
|
## Next
|
|
14
14
|
|
|
15
15
|
1. More MCP client discovery paths.
|
|
16
16
|
2. Rule packs mapped to MCP security best practices.
|
|
17
|
-
3.
|
|
18
|
-
4.
|
|
19
|
-
5.
|
|
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.
|
|
19
|
+
5. `mcp-guard audit` mode for client-ready reports.
|
|
20
20
|
|
|
21
21
|
## Later
|
|
22
22
|
|
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",
|
|
@@ -41,6 +43,7 @@
|
|
|
41
43
|
"src",
|
|
42
44
|
"README.md",
|
|
43
45
|
"action.yml",
|
|
46
|
+
"scripts/action-summary.js",
|
|
44
47
|
"LICENSE",
|
|
45
48
|
"SECURITY.md",
|
|
46
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 { generateHtmlReport, 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", "html"].includes(options.format)) {
|
|
84
|
-
throw new Error("--format must be one of: text, markdown, json, html");
|
|
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);
|
|
@@ -124,6 +124,9 @@ function renderReport(result, format) {
|
|
|
124
124
|
if (format === "html") {
|
|
125
125
|
return generateHtmlReport(result);
|
|
126
126
|
}
|
|
127
|
+
if (format === "sarif") {
|
|
128
|
+
return `${generateSarifReport(result)}\n`;
|
|
129
|
+
}
|
|
127
130
|
return generateTextReport(result);
|
|
128
131
|
}
|
|
129
132
|
|
|
@@ -145,7 +148,7 @@ Usage:
|
|
|
145
148
|
Scan options:
|
|
146
149
|
-c, --config <path> Scan a specific MCP config file. Can be repeated.
|
|
147
150
|
-o, --output <path> Write report to a file.
|
|
148
|
-
-f, --format <format> text, markdown, json, or
|
|
151
|
+
-f, --format <format> text, markdown, json, html, or sarif. Default: text.
|
|
149
152
|
--fail-on <severity> Exit 2 when finding severity is at least threshold.
|
|
150
153
|
critical, high, medium, low, none. Default: none.
|
|
151
154
|
--cwd <path> Working directory for project config discovery.
|
|
@@ -155,6 +158,7 @@ Examples:
|
|
|
155
158
|
mcp-guard scan
|
|
156
159
|
mcp-guard scan --format markdown --output mcp-guard-report.md
|
|
157
160
|
mcp-guard scan --format html --output mcp-guard-report.html
|
|
161
|
+
mcp-guard scan --format sarif --output mcp-guard.sarif
|
|
158
162
|
mcp-guard scan --config .mcp.json --fail-on high
|
|
159
163
|
`;
|
|
160
164
|
}
|
package/src/report.js
CHANGED
|
@@ -105,6 +105,40 @@ 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
|
+
|
|
108
142
|
export function generateHtmlReport(result) {
|
|
109
143
|
const safeResult = sanitizeResult(result);
|
|
110
144
|
const riskTone = riskToneForScore(safeResult.summary.riskScore);
|
|
@@ -491,6 +525,93 @@ function sanitizeResult(result) {
|
|
|
491
525
|
};
|
|
492
526
|
}
|
|
493
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
|
+
|
|
494
615
|
function metric(label, value) {
|
|
495
616
|
return `<div class="metric"><strong>${escapeHtml(value)}</strong><span>${escapeHtml(label)}</span></div>`;
|
|
496
617
|
}
|