Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ on:
paths:
- 'data/servers.json'
- 'README.md'
- 'scripts/**'
- 'tests/**'
schedule:
# Run weekly security scans on Mondays at 06:00 UTC
- cron: '0 6 * * 1'
Expand Down Expand Up @@ -46,6 +48,9 @@ jobs:
- name: Validate data
run: python scripts/validate.py

- name: Test security scanner
run: python -m unittest discover -s tests

security-scan:
name: Automated Security Scanning
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Find **secure MCP servers** for your agentic AI applications with confidence. Mo
| Server | Version | Security Status | Description |
|--------|---------|----------------|-------------|
| [Anthropic Computer Use](https://github.com/anthropics/anthropic-computer-use) | 0.1.0 | ⏳ Awaiting Scan | Desktop automation with screen capture and input control |
| [Xquik MCP Server](https://github.com/Xquik-dev/x-twitter-scraper) | 2.6.4 | ⏳ Awaiting Scan | Remote MCP server for authenticated X data workflows |

---

Expand Down Expand Up @@ -527,6 +528,5 @@ _Click on server scores above to jump to detailed security breakdowns:_






53 changes: 52 additions & 1 deletion data/servers.json
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,57 @@
"iam",
"enterprise"
]
},
{
"name": "Xquik MCP Server",
"slug": "xquik",
"repository": "https://github.com/Xquik-dev/x-twitter-scraper",
"category": "under-review",
"description": "Remote MCP server for authenticated X data workflows",
"maintainer": {
"name": "Xquik-dev",
"type": "organization",
"contact": "https://github.com/Xquik-dev"
},
"versions": [
{
"version": "2.6.4",
"release_date": "2026-08-12",
"security_status": "under-review",
"is_recommended": false,
"security_scan": {
"scan_date": "2026-08-12T18:05:02Z",
"scanner_version": "pending",
"static_analysis": {
"status": "not-applicable",
"details": "Awaiting project security validation",
"score": 50
},
"dependency_scan": {
"status": "not-applicable",
"details": "Awaiting project security validation",
"score": 50
},
"tool_poisoning_check": {
"status": "not-applicable",
"details": "Awaiting project security validation",
"score": 50
},
"overall_score": 50
},
"vulnerabilities": []
}
],
"mcp_protocol_versions": [
"2026-07-28"
],
"tags": [
"x",
"twitter",
"social-data",
"remote-mcp",
"api"
]
}
]
}
}
30 changes: 28 additions & 2 deletions scripts/security-scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ def _run_mcp_scan(self, repo_path: str) -> Dict:
return self._basic_tool_poisoning_check(repo_path)

try:
successful_scan_count = 0
# Run mcp-scan on found configuration files with enhanced options
for config_file in mcp_config_files:
cmd = ['uvx', 'mcp-scan@latest', 'scan', '--json', '--local-only', '--verbose', config_file]
Expand All @@ -242,8 +243,9 @@ def _run_mcp_scan(self, repo_path: str) -> Dict:
scan_output = json.loads(result.stdout)

# Parse mcp-scan results with severity breakdown
if 'results' in scan_output:
if isinstance(scan_output.get('results'), list):
config_results = scan_output['results']
successful_scan_count += 1
issues_found = 0
critical_issues = 0
high_issues = 0
Expand Down Expand Up @@ -276,6 +278,11 @@ def _run_mcp_scan(self, repo_path: str) -> Dict:
}

results['issues_found'] += issues_found
else:
logger.warning(f"MCP-scan returned an unexpected payload for {config_file}")
results['scan_results'][config_file] = {
'error': 'Scan output did not contain a results list'
}

except json.JSONDecodeError:
logger.warning(f"Could not parse mcp-scan output for {config_file}")
Expand All @@ -290,6 +297,19 @@ def _run_mcp_scan(self, repo_path: str) -> Dict:
results['scan_results'][config_file] = {
'error': f'Scan failed: {error_msg}'
}

failed_scan_count = len(mcp_config_files) - successful_scan_count
if successful_scan_count == 0:
fallback = self._basic_tool_poisoning_check(repo_path)
fallback['details'] = (
f'MCP-scan failed for all {failed_scan_count} configuration file(s); '
f"{fallback['details']}"
)
fallback['scan_results'] = results['scan_results']
if fallback['status'] == 'pass':
fallback['status'] = 'warning'
fallback['score'] = min(fallback['score'], 70)
return fallback

# Calculate severity-weighted score
total_critical = sum(config.get('critical_issues', 0) for config in results['scan_results'].values() if isinstance(config, dict))
Expand Down Expand Up @@ -326,6 +346,12 @@ def _run_mcp_scan(self, repo_path: str) -> Dict:
'details': f'MCP-scan found only low-severity issues ({total_issues} total)',
'score': max(80, score)
})

if failed_scan_count:
results['details'] += f'; {failed_scan_count} configuration scan(s) failed'
if results['status'] == 'pass':
results['status'] = 'warning'
results['score'] = min(results['score'], 75)

except subprocess.TimeoutExpired:
results.update({
Expand Down Expand Up @@ -1132,4 +1158,4 @@ def main():


if __name__ == '__main__':
main()
main()
56 changes: 56 additions & 0 deletions tests/test_security_scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import importlib.util
import json
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch


MODULE_PATH = Path(__file__).parents[1] / "scripts" / "security-scanner.py"
SPEC = importlib.util.spec_from_file_location("security_scanner", MODULE_PATH)
SECURITY_SCANNER = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(SECURITY_SCANNER)


class McpScanFailureTest(unittest.TestCase):
def test_all_failed_scans_use_basic_fallback(self):
with tempfile.TemporaryDirectory() as directory:
config_path = Path(directory) / "mcp.json"
config_path.write_text('{"mcpServers": {}}', encoding="utf-8")
failed = subprocess.CompletedProcess([], 2, "", "unsupported option")

with patch.object(SECURITY_SCANNER.subprocess, "run", return_value=failed):
result = SECURITY_SCANNER.SecurityScanner()._run_mcp_scan(directory)

self.assertEqual(result["status"], "warning")
self.assertEqual(result["score"], 70)
self.assertIn("failed for all 1 configuration file(s)", result["details"])
self.assertIn(str(config_path), result["scan_results"])

def test_partial_scan_failure_cannot_report_full_pass(self):
with tempfile.TemporaryDirectory() as directory:
first_config = Path(directory) / "mcp.json"
second_config = Path(directory) / "mcp_config.json"
first_config.write_text('{"mcpServers": {}}', encoding="utf-8")
second_config.write_text('{"mcpServers": {}}', encoding="utf-8")
succeeded = subprocess.CompletedProcess(
[], 0, json.dumps({"results": []}), ""
)
failed = subprocess.CompletedProcess([], 2, "", "scan failed")

with patch.object(
SECURITY_SCANNER.subprocess,
"run",
side_effect=[succeeded, failed],
):
result = SECURITY_SCANNER.SecurityScanner()._run_mcp_scan(directory)

self.assertEqual(result["status"], "warning")
self.assertEqual(result["score"], 75)
self.assertEqual(result["issues_found"], 0)
self.assertIn("1 configuration scan(s) failed", result["details"])


if __name__ == "__main__":
unittest.main()