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 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.2.0"><img alt="Release" src="https://img.shields.io/github/v/release/ChaoYue0307/mcp-guard?color=7c2d12"></a>
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.2.0
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 JSON output for local review and CI.
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
- 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
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
- mcp-guard scan "${scan_args[@]}" --fail-on "${MCP_GUARD_FAIL_ON}"
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: exit "${MCP_GUARD_EXIT_CODE}"
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 JSON scan reports;
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;
@@ -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 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.
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.2.0
25
+ - uses: ChaoYue0307/mcp-guard@v0.3.0
26
26
  with:
27
27
  fail-on: high
28
28
  ```
29
29
 
30
- ## Scan a Specific Config
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
- - uses: ChaoYue0307/mcp-guard@v0.2.0
34
- with:
35
- config: .mcp.json
36
- fail-on: medium
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
- ## Pin the npm Package
58
+ ## Report-Only Mode
40
59
 
41
- The action defaults to `agent-mcp-guard@latest`. Pin it when you want deterministic CI behavior:
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.2.0
63
+ - uses: ChaoYue0307/mcp-guard@v0.3.0
45
64
  with:
46
- package-version: 0.2.0
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, and redacted JSON output.
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, HTML, and JSON reports as artifacts.
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. `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.
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.2.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.2.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 html. Default: text.
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
  }