mcp-vulnerabilities 1.0.0__py3-none-any.whl

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.
Files changed (44) hide show
  1. mcp_vulnerabilities/__init__.py +51 -0
  2. mcp_vulnerabilities/audit/__init__.py +14 -0
  3. mcp_vulnerabilities/audit/fixer.py +164 -0
  4. mcp_vulnerabilities/audit/matcher.py +493 -0
  5. mcp_vulnerabilities/audit/parsers.py +179 -0
  6. mcp_vulnerabilities/audit/reporters.py +150 -0
  7. mcp_vulnerabilities/catalog.py +148 -0
  8. mcp_vulnerabilities/cli.py +800 -0
  9. mcp_vulnerabilities/converters/__init__.py +17 -0
  10. mcp_vulnerabilities/converters/cve_json5.py +246 -0
  11. mcp_vulnerabilities/converters/ghsa.py +217 -0
  12. mcp_vulnerabilities/converters/markdown_cve.py +334 -0
  13. mcp_vulnerabilities/converters/nvd.py +251 -0
  14. mcp_vulnerabilities/converters/osv_dev.py +206 -0
  15. mcp_vulnerabilities/converters/verity.py +120 -0
  16. mcp_vulnerabilities/deduplicator.py +275 -0
  17. mcp_vulnerabilities/discovery/__init__.py +8 -0
  18. mcp_vulnerabilities/discovery/enricher.py +97 -0
  19. mcp_vulnerabilities/discovery/github_curated.py +103 -0
  20. mcp_vulnerabilities/discovery/glama.py +74 -0
  21. mcp_vulnerabilities/discovery/npm.py +80 -0
  22. mcp_vulnerabilities/discovery/orchestrator.py +112 -0
  23. mcp_vulnerabilities/discovery/pypi.py +85 -0
  24. mcp_vulnerabilities/discovery/registries.py +105 -0
  25. mcp_vulnerabilities/discovery/smithery.py +85 -0
  26. mcp_vulnerabilities/feed.py +223 -0
  27. mcp_vulnerabilities/filter.py +158 -0
  28. mcp_vulnerabilities/models.py +346 -0
  29. mcp_vulnerabilities/notifier.py +124 -0
  30. mcp_vulnerabilities/pipeline.py +776 -0
  31. mcp_vulnerabilities/probe/__init__.py +0 -0
  32. mcp_vulnerabilities/probe/client.py +83 -0
  33. mcp_vulnerabilities/probe/fingerprinter.py +104 -0
  34. mcp_vulnerabilities/probe/transport.py +135 -0
  35. mcp_vulnerabilities/site_builder.py +137 -0
  36. mcp_vulnerabilities/snapshot.py +328 -0
  37. mcp_vulnerabilities/state.py +116 -0
  38. mcp_vulnerabilities/transitive.py +296 -0
  39. mcp_vulnerabilities/validator.py +186 -0
  40. mcp_vulnerabilities-1.0.0.dist-info/METADATA +110 -0
  41. mcp_vulnerabilities-1.0.0.dist-info/RECORD +44 -0
  42. mcp_vulnerabilities-1.0.0.dist-info/WHEEL +5 -0
  43. mcp_vulnerabilities-1.0.0.dist-info/entry_points.txt +2 -0
  44. mcp_vulnerabilities-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,51 @@
1
+ """Open Source Vulnerability (OSV v1.6.0) ingestion and normalization engine."""
2
+
3
+ from mcp_vulnerabilities.converters.ghsa import GhsaConverter
4
+ from mcp_vulnerabilities.converters.markdown_cve import MarkdownAdvisoryConverter
5
+ from mcp_vulnerabilities.converters.nvd import NvdConverter
6
+ from mcp_vulnerabilities.converters.osv_dev import OsvDevConverter
7
+ from mcp_vulnerabilities.converters.verity import VerityCatalogConverter
8
+ from mcp_vulnerabilities.deduplicator import OsvDeduplicator
9
+ from mcp_vulnerabilities.models import (
10
+ AffectedPackage,
11
+ DatabaseSpecificMcp,
12
+ EventSpec,
13
+ OsvVulnerability,
14
+ PackageSpec,
15
+ RangeSpec,
16
+ RangeType,
17
+ ReferenceSpec,
18
+ ReferenceType,
19
+ SeveritySpec,
20
+ SeverityType,
21
+ build_purl,
22
+ parse_purl,
23
+ )
24
+ from mcp_vulnerabilities.pipeline import McpVulnerabilityPipeline, PipelineResult
25
+ from mcp_vulnerabilities.validator import OsvValidationError, OsvValidator
26
+
27
+ __all__ = [
28
+ "AffectedPackage",
29
+ "DatabaseSpecificMcp",
30
+ "EventSpec",
31
+ "GhsaConverter",
32
+ "MarkdownAdvisoryConverter",
33
+ "McpVulnerabilityPipeline",
34
+ "NvdConverter",
35
+ "OsvDeduplicator",
36
+ "OsvDevConverter",
37
+ "OsvValidationError",
38
+ "OsvValidator",
39
+ "OsvVulnerability",
40
+ "PackageSpec",
41
+ "PipelineResult",
42
+ "RangeSpec",
43
+ "RangeType",
44
+ "ReferenceSpec",
45
+ "ReferenceType",
46
+ "SeveritySpec",
47
+ "SeverityType",
48
+ "VerityCatalogConverter",
49
+ "build_purl",
50
+ "parse_purl",
51
+ ]
@@ -0,0 +1,14 @@
1
+ """Audit subpackage for MCP client configs and installed packages."""
2
+
3
+ from .fixer import ClientConfigFixer
4
+ from .matcher import AuditFinding, AuditReport, VulnerabilityMatcher
5
+ from .parsers import ClientConfigParser, DiscoveredClientServer
6
+
7
+ __all__ = [
8
+ "AuditFinding",
9
+ "AuditReport",
10
+ "ClientConfigFixer",
11
+ "ClientConfigParser",
12
+ "DiscoveredClientServer",
13
+ "VulnerabilityMatcher",
14
+ ]
@@ -0,0 +1,164 @@
1
+ """Automated remediation for vulnerable MCP servers in client configurations."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import re
7
+ import shutil
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from packaging.version import InvalidVersion, Version
13
+
14
+ from .matcher import VulnerabilityMatcher, AuditFinding
15
+ from .parsers import ClientConfigParser
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ClientConfigFixer:
21
+ """Identifies and remediates vulnerable MCP servers in client configurations."""
22
+
23
+ def __init__(self, matcher: VulnerabilityMatcher):
24
+ self.matcher = matcher
25
+
26
+ def _calculate_minimum_safe_version(self, findings: list[AuditFinding]) -> str | None:
27
+ """Calculates the highest fixed version among all findings."""
28
+ if not findings:
29
+ return None
30
+
31
+ max_version: Version | None = None
32
+ best_version_str: str | None = None
33
+
34
+ for f in findings:
35
+ if not f.fixed_version:
36
+ continue
37
+
38
+ try:
39
+ v = Version(f.fixed_version)
40
+ if max_version is None or v > max_version:
41
+ max_version = v
42
+ best_version_str = f.fixed_version
43
+ except InvalidVersion:
44
+ # If we can't parse semver, just take the first one or continue
45
+ if max_version is None:
46
+ best_version_str = f.fixed_version
47
+
48
+ return best_version_str
49
+
50
+ def fix_config(self, config_path: Path, dry_run: bool = False) -> dict[str, Any]:
51
+ """Fixes a client configuration file by replacing vulnerable versions."""
52
+ if not config_path.exists():
53
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
54
+
55
+ servers = ClientConfigParser.parse_file(config_path)
56
+ with open(config_path, "r", encoding="utf-8") as f:
57
+ data = json.load(f)
58
+
59
+ report = self.matcher.audit_servers(servers)
60
+
61
+ # Group findings by server name
62
+ findings_by_server: dict[str, list[AuditFinding]] = {}
63
+ for f in report.findings:
64
+ findings_by_server.setdefault(f.server_name, []).append(f)
65
+
66
+ changes_made = []
67
+
68
+ # We need to find the specific mcpServers key or use root
69
+ raw_servers = data.get("mcpServers") or data.get("mcp_servers") or data.get("servers")
70
+
71
+
72
+ if not isinstance(raw_servers, dict):
73
+ if all(isinstance(v, dict) and ("command" in v or "args" in v) for v in data.values()):
74
+ raw_servers = data
75
+
76
+ else:
77
+ return {"status": "no_servers_found"}
78
+
79
+ for server in servers:
80
+ findings = findings_by_server.get(server.server_name)
81
+ if not findings:
82
+ continue
83
+
84
+ safe_version = self._calculate_minimum_safe_version(findings)
85
+ if not safe_version:
86
+ logger.warning(f"No safe version found for {server.server_name}, skipping.")
87
+ continue
88
+
89
+ # If the current version is already >= safe_version (e.g. unpinned), skip
90
+ try:
91
+ if server.version and Version(server.version) >= Version(safe_version):
92
+ continue
93
+ except InvalidVersion:
94
+ pass
95
+
96
+ old_version = server.version or "unpinned"
97
+
98
+ # Find the args in the original dict
99
+ srv_dict = raw_servers.get(server.server_name)
100
+ if not srv_dict or not isinstance(srv_dict, dict):
101
+ continue
102
+
103
+ args = srv_dict.get("args", [])
104
+ new_args = list(args)
105
+
106
+ # We need to rewrite the args
107
+ # Simple approach: find the arg that contains the package name and old version
108
+ for i, arg in enumerate(new_args):
109
+ if server.package_name in str(arg):
110
+ # Rewrite this arg based on ecosystem
111
+ if server.ecosystem == "npm":
112
+ if "@" in server.package_name and server.package_name.startswith("@"):
113
+ # scoped package
114
+ new_args[i] = f"{server.package_name}@{safe_version}"
115
+ else:
116
+ new_args[i] = f"{server.package_name}@{safe_version}"
117
+ elif server.ecosystem == "PyPI":
118
+ # We use == for pipx/uvx
119
+ if "==" in arg or ">=" in arg:
120
+ new_args[i] = re.sub(r"(==|>=|<=|>|<|~=|@).*", f"=={safe_version}", arg)
121
+ else:
122
+ new_args[i] = f"{arg}=={safe_version}"
123
+ elif server.ecosystem == "Docker":
124
+ if ":" in arg:
125
+ pkg = arg.rsplit(":", 1)[0]
126
+ new_args[i] = f"{pkg}:{safe_version}"
127
+ else:
128
+ new_args[i] = f"{arg}:{safe_version}"
129
+ break
130
+
131
+ srv_dict["args"] = new_args
132
+ changes_made.append({
133
+ "server_name": server.server_name,
134
+ "package_name": server.package_name,
135
+ "old_version": old_version,
136
+ "new_version": safe_version
137
+ })
138
+
139
+ if not changes_made:
140
+ return {"status": "no_changes_needed", "changes": []}
141
+
142
+ if not dry_run:
143
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
144
+ backup_path = config_path.with_name(f"{config_path.name}.bak.{timestamp}")
145
+ shutil.copy2(config_path, backup_path)
146
+
147
+ # Atomically write
148
+ tmp_path = config_path.with_name(f"{config_path.name}.tmp")
149
+ with open(tmp_path, "w", encoding="utf-8") as f:
150
+ json.dump(data, f, indent=2)
151
+ f.write("\n")
152
+ os.replace(tmp_path, config_path)
153
+
154
+ return {
155
+ "status": "success",
156
+ "changes": changes_made,
157
+ "backup_path": str(backup_path)
158
+ }
159
+
160
+ return {
161
+ "status": "dry_run",
162
+ "changes": changes_made
163
+ }
164
+