@sms-florin/mcp-ssrf-scan 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
@@ -59,6 +59,24 @@ Exit code is `1` if any `HIGH` finding is reported, `0` otherwise:
59
59
 
60
60
  `--json` prints findings as JSON instead of the human-readable list.
61
61
 
62
+ `--sarif` prints SARIF 2.1.0 for code-scanning UIs. Secret findings are tagged
63
+ OWASP MCP01 (Token Mismanagement & Secret Exposure) and exec findings MCP05
64
+ (Command Injection & Execution). The OWASP MCP Top 10 has no SSRF entry, so
65
+ the SSRF rule carries no OWASP tag.
66
+
67
+ ### GitLab CI component
68
+
69
+ ```yaml
70
+ include:
71
+ - component: gitlab.com/florin-builds/mcp-ssrf-scan/scan@~latest
72
+ inputs:
73
+ path: ./src
74
+ ```
75
+
76
+ Inputs: `stage` (default `test`), `path` (default `.`), `node_image` (default
77
+ `node:22-alpine`). The job fails on any HIGH finding and keeps
78
+ `mcp-ssrf-scan.sarif` as an artifact.
79
+
62
80
  ## mcp-ssrf-scan Cloud (optional, paid)
63
81
 
64
82
  The CLI above is free and stays free. [mcp-ssrf-scan Cloud](https://mcpscan.flo-voice1.com)
package/dist/cli.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync } from "node:child_process";
3
+ import { readFileSync } from "node:fs";
4
+ import { toSarif } from "./sarif.js";
3
5
  import { scanDirectory } from "./scan.js";
4
6
  const COLOR = {
5
7
  high: "\x1b[31m", // red
@@ -32,6 +34,8 @@ finding as "go read this function," not as a confirmed bug.
32
34
 
33
35
  Options:
34
36
  --json Print findings as JSON instead of the human-readable list
37
+ --sarif Print findings as SARIF 2.1.0 (GitHub/GitLab code scanning,
38
+ VS Code); rules carry OWASP MCP Top 10 tags where one fits
35
39
  --upload Upload this scan to mcp-ssrf-scan Cloud (needs MCP_SSRF_SCAN_KEY)
36
40
  and print what's new/fixed since the last scan of this repo
37
41
  --repo owner/name Repo name for --upload (auto-detected in GitHub Actions,
@@ -121,9 +125,14 @@ async function main() {
121
125
  const valueFlags = new Set(["--repo"]);
122
126
  const target = args.find((a, i) => !a.startsWith("-") && !valueFlags.has(args[i - 1])) ?? ".";
123
127
  const asJson = args.includes("--json");
128
+ const asSarif = args.includes("--sarif");
124
129
  const findings = scanDirectory(target);
125
130
  const hasHigh = findings.some((f) => f.severity === "high");
126
- if (asJson) {
131
+ if (asSarif) {
132
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
133
+ console.log(JSON.stringify(toSarif(findings, pkg.version), null, 2));
134
+ }
135
+ else if (asJson) {
127
136
  console.log(JSON.stringify({ target, findings }, null, 2));
128
137
  }
129
138
  else {
@@ -0,0 +1,44 @@
1
+ import type { Finding } from "./scan.js";
2
+ export declare function toSarif(findings: Finding[], version: string): {
3
+ $schema: string;
4
+ version: string;
5
+ runs: {
6
+ tool: {
7
+ driver: {
8
+ name: string;
9
+ version: string;
10
+ informationUri: string;
11
+ rules: {
12
+ id: "ssrf" | "secret" | "exec-injection";
13
+ name: string;
14
+ shortDescription: {
15
+ text: string;
16
+ };
17
+ properties: {
18
+ tags: string[];
19
+ };
20
+ }[];
21
+ };
22
+ };
23
+ results: {
24
+ ruleId: "ssrf" | "secret" | "exec-injection";
25
+ level: string;
26
+ message: {
27
+ text: string;
28
+ };
29
+ locations: {
30
+ physicalLocation: {
31
+ artifactLocation: {
32
+ uri: string;
33
+ };
34
+ region: {
35
+ startLine: number;
36
+ snippet: {
37
+ text: string;
38
+ };
39
+ };
40
+ };
41
+ }[];
42
+ }[];
43
+ }[];
44
+ };
package/dist/sarif.js ADDED
@@ -0,0 +1,55 @@
1
+ // OWASP MCP Top 10 (2025) has no SSRF entry, so that rule is left unmapped
2
+ // rather than forced into a category it doesn't belong to.
3
+ const RULES = {
4
+ ssrf: {
5
+ name: "McpToolSsrf",
6
+ description: "Outbound request in a tool handler uses a caller-controlled URL with no host/IP validation.",
7
+ },
8
+ secret: {
9
+ name: "HardcodedSecret",
10
+ description: "Hardcoded credential or API key in source.",
11
+ owasp: "MCP01:2025 Token Mismanagement & Secret Exposure",
12
+ },
13
+ "exec-injection": {
14
+ name: "ExecInjection",
15
+ description: "exec()/Function() call next to a template literal, possible command injection.",
16
+ owasp: "MCP05:2025 Command Injection & Execution",
17
+ },
18
+ };
19
+ const LEVEL = { high: "error", medium: "warning", info: "note" };
20
+ export function toSarif(findings, version) {
21
+ return {
22
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
23
+ version: "2.1.0",
24
+ runs: [
25
+ {
26
+ tool: {
27
+ driver: {
28
+ name: "mcp-ssrf-scan",
29
+ version,
30
+ informationUri: "https://gitlab.com/florin-builds/mcp-ssrf-scan",
31
+ rules: Object.keys(RULES).map((id) => ({
32
+ id,
33
+ name: RULES[id].name,
34
+ shortDescription: { text: RULES[id].description },
35
+ properties: RULES[id].owasp ? { tags: ["security", RULES[id].owasp] } : { tags: ["security"] },
36
+ })),
37
+ },
38
+ },
39
+ results: findings.map((f) => ({
40
+ ruleId: f.rule,
41
+ level: LEVEL[f.severity],
42
+ message: { text: f.message },
43
+ locations: [
44
+ {
45
+ physicalLocation: {
46
+ artifactLocation: { uri: f.file },
47
+ region: { startLine: f.line, snippet: { text: f.snippet } },
48
+ },
49
+ },
50
+ ],
51
+ })),
52
+ },
53
+ ],
54
+ };
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sms-florin/mcp-ssrf-scan",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A static scanner for MCP servers that catches the exact bug class behind most real MCP SSRF reports: a tool handler making an outbound request to a caller-controlled URL with no host validation.",
5
5
  "type": "module",
6
6
  "bin": {