iflow-mcp_anton-prosterity-documentation-search-enhanced 1.9.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 (26) hide show
  1. documentation_search_enhanced/__init__.py +14 -0
  2. documentation_search_enhanced/__main__.py +6 -0
  3. documentation_search_enhanced/config.json +1674 -0
  4. documentation_search_enhanced/config_manager.py +233 -0
  5. documentation_search_enhanced/config_validator.py +79 -0
  6. documentation_search_enhanced/content_enhancer.py +578 -0
  7. documentation_search_enhanced/docker_manager.py +87 -0
  8. documentation_search_enhanced/logger.py +179 -0
  9. documentation_search_enhanced/main.py +2170 -0
  10. documentation_search_enhanced/project_generator.py +260 -0
  11. documentation_search_enhanced/project_scanner.py +85 -0
  12. documentation_search_enhanced/reranker.py +230 -0
  13. documentation_search_enhanced/site_index_builder.py +274 -0
  14. documentation_search_enhanced/site_index_downloader.py +222 -0
  15. documentation_search_enhanced/site_search.py +1325 -0
  16. documentation_search_enhanced/smart_search.py +473 -0
  17. documentation_search_enhanced/snyk_integration.py +657 -0
  18. documentation_search_enhanced/vector_search.py +303 -0
  19. documentation_search_enhanced/version_resolver.py +189 -0
  20. documentation_search_enhanced/vulnerability_scanner.py +545 -0
  21. documentation_search_enhanced/web_scraper.py +117 -0
  22. iflow_mcp_anton_prosterity_documentation_search_enhanced-1.9.0.dist-info/METADATA +195 -0
  23. iflow_mcp_anton_prosterity_documentation_search_enhanced-1.9.0.dist-info/RECORD +26 -0
  24. iflow_mcp_anton_prosterity_documentation_search_enhanced-1.9.0.dist-info/WHEEL +4 -0
  25. iflow_mcp_anton_prosterity_documentation_search_enhanced-1.9.0.dist-info/entry_points.txt +2 -0
  26. iflow_mcp_anton_prosterity_documentation_search_enhanced-1.9.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,657 @@
1
+ """
2
+ Snyk integration for enhanced security scanning.
3
+ Provides comprehensive vulnerability analysis, license compliance, and security monitoring.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import sys
9
+ import httpx
10
+ from datetime import datetime, timedelta
11
+ from typing import Dict, List, Any, Optional, Tuple
12
+ from dataclasses import dataclass
13
+ from enum import Enum
14
+
15
+ from .vulnerability_scanner import Vulnerability, SeverityLevel, SecurityReport
16
+ from .project_scanner import find_and_parse_dependencies
17
+
18
+
19
+ class SnykSeverity(Enum):
20
+ """Snyk severity levels mapping"""
21
+
22
+ CRITICAL = "critical"
23
+ HIGH = "high"
24
+ MEDIUM = "medium"
25
+ LOW = "low"
26
+
27
+
28
+ @dataclass
29
+ class SnykVulnerability:
30
+ """Snyk-specific vulnerability data"""
31
+
32
+ id: str
33
+ title: str
34
+ description: str
35
+ severity: SnykSeverity
36
+ cvss_score: Optional[float]
37
+ cve: List[str]
38
+ cwe: List[str]
39
+ exploit_maturity: Optional[str]
40
+ patches: List[str]
41
+ upgrade_path: List[str]
42
+ is_patchable: bool
43
+ is_pinnable: bool
44
+ published_date: str
45
+ disclosure_time: Optional[str]
46
+
47
+ def to_vulnerability(self) -> Vulnerability:
48
+ """Convert to standard Vulnerability format"""
49
+ severity_map = {
50
+ SnykSeverity.CRITICAL: SeverityLevel.CRITICAL,
51
+ SnykSeverity.HIGH: SeverityLevel.HIGH,
52
+ SnykSeverity.MEDIUM: SeverityLevel.MEDIUM,
53
+ SnykSeverity.LOW: SeverityLevel.LOW,
54
+ }
55
+
56
+ return Vulnerability(
57
+ id=self.id,
58
+ title=self.title,
59
+ description=self.description,
60
+ severity=severity_map[self.severity],
61
+ cvss_score=self.cvss_score,
62
+ cve_id=self.cve[0] if self.cve else None,
63
+ affected_versions=["various"], # Snyk provides more complex version info
64
+ fixed_version=self.upgrade_path[-1] if self.upgrade_path else None,
65
+ published_date=self.published_date,
66
+ source="snyk",
67
+ references=[f"https://snyk.io/vuln/{self.id}"],
68
+ )
69
+
70
+
71
+ @dataclass
72
+ class SnykLicense:
73
+ """License information from Snyk"""
74
+
75
+ id: str
76
+ name: str
77
+ spdx_id: Optional[str]
78
+ type: str # "copyleft", "permissive", "proprietary", etc.
79
+ url: Optional[str]
80
+ is_deprecated: bool
81
+ instructions: str
82
+
83
+
84
+ @dataclass
85
+ class SnykPackageInfo:
86
+ """Package information with security details"""
87
+
88
+ name: str
89
+ version: str
90
+ ecosystem: str
91
+ vulnerabilities: List[SnykVulnerability]
92
+ licenses: List[SnykLicense]
93
+ severity_counts: Dict[str, int]
94
+ dependency_paths: List[List[str]]
95
+ is_direct_dependency: bool
96
+
97
+
98
+ class SnykIntegration:
99
+ """Snyk API integration for enterprise security scanning"""
100
+
101
+ def __init__(self):
102
+ self.api_key = os.getenv("SNYK_API_KEY")
103
+ self.org_id = os.getenv("SNYK_ORG_ID")
104
+ self.base_url = "https://api.snyk.io"
105
+ self.rest_api_url = "https://api.snyk.io/rest"
106
+ self.timeout = httpx.Timeout(60.0)
107
+
108
+ # Cache for API responses
109
+ self.cache = {}
110
+ self.cache_ttl = timedelta(hours=6)
111
+
112
+ def _get_headers(self) -> Dict[str, str]:
113
+ """Get authentication headers for Snyk API"""
114
+ if not self.api_key:
115
+ raise ValueError("SNYK_API_KEY environment variable is required")
116
+
117
+ return {
118
+ "Authorization": f"token {self.api_key}",
119
+ "Content-Type": "application/json",
120
+ "User-Agent": "documentation-search-enhanced/1.3.0",
121
+ }
122
+
123
+ async def test_connection(self) -> Dict[str, Any]:
124
+ """Test Snyk API connection and get organization info"""
125
+ try:
126
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
127
+ response = await client.get(
128
+ f"{self.base_url}/v1/user/me", headers=self._get_headers()
129
+ )
130
+
131
+ if response.status_code == 200:
132
+ user_data = response.json()
133
+
134
+ # Get organizations
135
+ orgs_response = await client.get(
136
+ f"{self.base_url}/v1/user/me/orgs", headers=self._get_headers()
137
+ )
138
+
139
+ orgs_data = (
140
+ orgs_response.json()
141
+ if orgs_response.status_code == 200
142
+ else {"orgs": []}
143
+ )
144
+
145
+ return {
146
+ "status": "connected",
147
+ "user": user_data.get("username"),
148
+ "organizations": [
149
+ {"id": org["id"], "name": org["name"]}
150
+ for org in orgs_data.get("orgs", [])
151
+ ],
152
+ }
153
+ else:
154
+ return {
155
+ "status": "error",
156
+ "error": f"Authentication failed: {response.status_code}",
157
+ }
158
+
159
+ except Exception as e:
160
+ return {"status": "error", "error": f"Connection failed: {str(e)}"}
161
+
162
+ async def scan_package(
163
+ self, package_name: str, version: str, ecosystem: str = "pypi"
164
+ ) -> SnykPackageInfo:
165
+ """Scan a single package for vulnerabilities"""
166
+ cache_key = f"package_{ecosystem}_{package_name}_{version}"
167
+
168
+ if self._is_cached(cache_key):
169
+ return self.cache[cache_key]["data"]
170
+
171
+ ecosystem_map = {
172
+ "pypi": "pip",
173
+ "npm": "npm",
174
+ "maven": "maven",
175
+ "gradle": "gradle",
176
+ "nuget": "nuget",
177
+ }
178
+
179
+ snyk_ecosystem = ecosystem_map.get(ecosystem.lower(), "pip")
180
+
181
+ try:
182
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
183
+ # Test endpoint for vulnerabilities
184
+ test_payload = {
185
+ "encoding": "plain",
186
+ "files": {
187
+ (
188
+ "requirements.txt"
189
+ if snyk_ecosystem == "pip"
190
+ else "package.json"
191
+ ): {
192
+ "contents": (
193
+ f"{package_name}=={version}"
194
+ if snyk_ecosystem == "pip"
195
+ else json.dumps(
196
+ {"dependencies": {package_name: version}}
197
+ )
198
+ )
199
+ }
200
+ },
201
+ }
202
+
203
+ response = await client.post(
204
+ f"{self.base_url}/v1/test/{snyk_ecosystem}",
205
+ headers=self._get_headers(),
206
+ json=test_payload,
207
+ )
208
+
209
+ if response.status_code == 200:
210
+ data = response.json()
211
+ package_info = self._parse_package_scan_result(
212
+ data, package_name, version, ecosystem
213
+ )
214
+
215
+ # Cache the result
216
+ self._cache_result(cache_key, package_info)
217
+
218
+ return package_info
219
+ else:
220
+ # Return empty result for failed scans
221
+ return SnykPackageInfo(
222
+ name=package_name,
223
+ version=version,
224
+ ecosystem=ecosystem,
225
+ vulnerabilities=[],
226
+ licenses=[],
227
+ severity_counts={
228
+ "critical": 0,
229
+ "high": 0,
230
+ "medium": 0,
231
+ "low": 0,
232
+ },
233
+ dependency_paths=[],
234
+ is_direct_dependency=True,
235
+ )
236
+
237
+ except Exception as e:
238
+ print(f"Snyk scan error for {package_name}: {e}", file=sys.stderr)
239
+ return SnykPackageInfo(
240
+ name=package_name,
241
+ version=version,
242
+ ecosystem=ecosystem,
243
+ vulnerabilities=[],
244
+ licenses=[],
245
+ severity_counts={"critical": 0, "high": 0, "medium": 0, "low": 0},
246
+ dependency_paths=[],
247
+ is_direct_dependency=True,
248
+ )
249
+
250
+ async def scan_project_manifest(
251
+ self, manifest_path: str, ecosystem: str = None
252
+ ) -> Dict[str, Any]:
253
+ """Scan project manifest file (requirements.txt, package.json, etc.)"""
254
+
255
+ if not os.path.exists(manifest_path):
256
+ return {"error": f"Manifest file not found: {manifest_path}"}
257
+
258
+ # Auto-detect ecosystem if not provided
259
+ if not ecosystem:
260
+ if manifest_path.endswith(("requirements.txt", "pyproject.toml")):
261
+ ecosystem = "pip"
262
+ elif manifest_path.endswith("package.json"):
263
+ ecosystem = "npm"
264
+ else:
265
+ ecosystem = "pip" # Default
266
+
267
+ try:
268
+ with open(manifest_path, "r", encoding="utf-8") as f:
269
+ file_contents = f.read()
270
+
271
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
272
+ test_payload = {
273
+ "encoding": "plain",
274
+ "files": {
275
+ os.path.basename(manifest_path): {"contents": file_contents}
276
+ },
277
+ }
278
+
279
+ response = await client.post(
280
+ f"{self.base_url}/v1/test/{ecosystem}",
281
+ headers=self._get_headers(),
282
+ json=test_payload,
283
+ )
284
+
285
+ if response.status_code == 200:
286
+ data = response.json()
287
+ return self._parse_project_scan_result(data, manifest_path)
288
+ else:
289
+ return {
290
+ "error": f"Snyk API error: {response.status_code}",
291
+ "details": response.text,
292
+ }
293
+
294
+ except Exception as e:
295
+ return {"error": f"Failed to scan manifest: {str(e)}"}
296
+
297
+ async def get_license_compliance(
298
+ self, packages: List[Tuple[str, str]], ecosystem: str = "pypi"
299
+ ) -> Dict[str, Any]:
300
+ """Check license compliance for multiple packages"""
301
+ compliance_results = {
302
+ "total_packages": len(packages),
303
+ "compliant_packages": 0,
304
+ "non_compliant_packages": 0,
305
+ "unknown_licenses": 0,
306
+ "license_summary": {},
307
+ "compliance_details": [],
308
+ }
309
+
310
+ # Define license policies (configurable)
311
+ allowed_licenses = {
312
+ "MIT",
313
+ "Apache-2.0",
314
+ "BSD-2-Clause",
315
+ "BSD-3-Clause",
316
+ "ISC",
317
+ "Unlicense",
318
+ "WTFPL",
319
+ }
320
+
321
+ restricted_licenses = {
322
+ "GPL-2.0",
323
+ "GPL-3.0",
324
+ "LGPL-2.1",
325
+ "LGPL-3.0",
326
+ "AGPL-3.0",
327
+ "SSPL-1.0",
328
+ }
329
+
330
+ for package_name, version in packages:
331
+ try:
332
+ package_info = await self.scan_package(package_name, version, ecosystem)
333
+
334
+ package_compliance = {
335
+ "package": package_name,
336
+ "version": version,
337
+ "licenses": [license.name for license in package_info.licenses],
338
+ "compliance_status": "unknown",
339
+ "risk_level": "unknown",
340
+ }
341
+
342
+ if package_info.licenses:
343
+ license_names = {license.name for license in package_info.licenses}
344
+
345
+ if license_names.intersection(restricted_licenses):
346
+ package_compliance["compliance_status"] = "non-compliant"
347
+ package_compliance["risk_level"] = "high"
348
+ compliance_results["non_compliant_packages"] += 1
349
+ elif license_names.intersection(allowed_licenses):
350
+ package_compliance["compliance_status"] = "compliant"
351
+ package_compliance["risk_level"] = "low"
352
+ compliance_results["compliant_packages"] += 1
353
+ else:
354
+ package_compliance["compliance_status"] = "review_required"
355
+ package_compliance["risk_level"] = "medium"
356
+ compliance_results["unknown_licenses"] += 1
357
+ else:
358
+ compliance_results["unknown_licenses"] += 1
359
+
360
+ compliance_results["compliance_details"].append(package_compliance)
361
+
362
+ # Update license summary
363
+ for license in package_info.licenses:
364
+ if license.name not in compliance_results["license_summary"]:
365
+ compliance_results["license_summary"][license.name] = 0
366
+ compliance_results["license_summary"][license.name] += 1
367
+
368
+ except Exception as e:
369
+ print(f"License check error for {package_name}: {e}", file=sys.stderr)
370
+
371
+ return compliance_results
372
+
373
+ async def monitor_project(self, project_path: str) -> Dict[str, Any]:
374
+ """Set up continuous monitoring for a project"""
375
+
376
+ # Find project dependencies
377
+ dep_result = find_and_parse_dependencies(project_path)
378
+ if not dep_result:
379
+ return {"error": "No supported dependency files found"}
380
+
381
+ filename, ecosystem, dependencies = dep_result
382
+
383
+ try:
384
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
385
+ # Import project for monitoring
386
+ import_payload = {
387
+ "target": {
388
+ "files": [
389
+ {
390
+ "path": filename,
391
+ "contents": self._generate_manifest_content(
392
+ dependencies, ecosystem
393
+ ),
394
+ }
395
+ ]
396
+ }
397
+ }
398
+
399
+ if not self.org_id:
400
+ return {
401
+ "error": "SNYK_ORG_ID environment variable is required for monitoring"
402
+ }
403
+
404
+ response = await client.post(
405
+ f"{self.rest_api_url}/orgs/{self.org_id}/projects",
406
+ headers=self._get_headers(),
407
+ json=import_payload,
408
+ )
409
+
410
+ if response.status_code == 201:
411
+ project_data = response.json()
412
+ return {
413
+ "status": "monitoring_enabled",
414
+ "project_id": project_data.get("data", {}).get("id"),
415
+ "project_name": os.path.basename(project_path),
416
+ "dependencies_count": len(dependencies),
417
+ }
418
+ else:
419
+ return {
420
+ "error": f"Failed to enable monitoring: {response.status_code}",
421
+ "details": response.text,
422
+ }
423
+
424
+ except Exception as e:
425
+ return {"error": f"Monitoring setup failed: {str(e)}"}
426
+
427
+ def _parse_package_scan_result(
428
+ self, scan_data: Dict[str, Any], package_name: str, version: str, ecosystem: str
429
+ ) -> SnykPackageInfo:
430
+ """Parse Snyk scan result into SnykPackageInfo"""
431
+
432
+ vulnerabilities = []
433
+ licenses = []
434
+ severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
435
+
436
+ # Parse vulnerabilities
437
+ for issue in scan_data.get("issues", {}).get("vulnerabilities", []):
438
+ vuln = self._parse_vulnerability(issue)
439
+ vulnerabilities.append(vuln)
440
+
441
+ severity_key = vuln.severity.value
442
+ if severity_key in severity_counts:
443
+ severity_counts[severity_key] += 1
444
+
445
+ # Parse licenses
446
+ for license_data in scan_data.get("issues", {}).get("licenses", []):
447
+ license = self._parse_license(license_data)
448
+ licenses.append(license)
449
+
450
+ return SnykPackageInfo(
451
+ name=package_name,
452
+ version=version,
453
+ ecosystem=ecosystem,
454
+ vulnerabilities=vulnerabilities,
455
+ licenses=licenses,
456
+ severity_counts=severity_counts,
457
+ dependency_paths=[], # Would need more complex parsing
458
+ is_direct_dependency=True,
459
+ )
460
+
461
+ def _parse_project_scan_result(
462
+ self, scan_data: Dict[str, Any], manifest_path: str
463
+ ) -> Dict[str, Any]:
464
+ """Parse project-level scan results"""
465
+
466
+ result = {
467
+ "manifest_file": manifest_path,
468
+ "scan_timestamp": datetime.now().isoformat(),
469
+ "summary": {
470
+ "total_dependencies": scan_data.get("dependencyCount", 0),
471
+ "vulnerability_count": len(
472
+ scan_data.get("issues", {}).get("vulnerabilities", [])
473
+ ),
474
+ "license_issues": len(scan_data.get("issues", {}).get("licenses", [])),
475
+ },
476
+ "vulnerabilities": [],
477
+ "license_issues": [],
478
+ "remediation": scan_data.get("remediation", {}),
479
+ }
480
+
481
+ # Process vulnerabilities
482
+ for vuln_data in scan_data.get("issues", {}).get("vulnerabilities", []):
483
+ vuln = self._parse_vulnerability(vuln_data)
484
+ result["vulnerabilities"].append(vuln.to_vulnerability().to_dict())
485
+
486
+ # Process license issues
487
+ for license_data in scan_data.get("issues", {}).get("licenses", []):
488
+ license = self._parse_license(license_data)
489
+ result["license_issues"].append(
490
+ {
491
+ "id": license.id,
492
+ "name": license.name,
493
+ "type": license.type,
494
+ "is_deprecated": license.is_deprecated,
495
+ }
496
+ )
497
+
498
+ return result
499
+
500
+ def _parse_vulnerability(self, vuln_data: Dict[str, Any]) -> SnykVulnerability:
501
+ """Parse vulnerability data from Snyk response"""
502
+
503
+ severity_map = {
504
+ "critical": SnykSeverity.CRITICAL,
505
+ "high": SnykSeverity.HIGH,
506
+ "medium": SnykSeverity.MEDIUM,
507
+ "low": SnykSeverity.LOW,
508
+ }
509
+
510
+ return SnykVulnerability(
511
+ id=vuln_data.get("id", ""),
512
+ title=vuln_data.get("title", ""),
513
+ description=vuln_data.get("description", ""),
514
+ severity=severity_map.get(
515
+ vuln_data.get("severity", "medium"), SnykSeverity.MEDIUM
516
+ ),
517
+ cvss_score=vuln_data.get("cvssScore"),
518
+ cve=vuln_data.get("identifiers", {}).get("CVE", []),
519
+ cwe=vuln_data.get("identifiers", {}).get("CWE", []),
520
+ exploit_maturity=vuln_data.get("exploitMaturity"),
521
+ patches=vuln_data.get("patches", []),
522
+ upgrade_path=vuln_data.get("upgradePath", []),
523
+ is_patchable=vuln_data.get("isPatchable", False),
524
+ is_pinnable=vuln_data.get("isPinnable", False),
525
+ published_date=vuln_data.get("publicationTime", ""),
526
+ disclosure_time=vuln_data.get("disclosureTime"),
527
+ )
528
+
529
+ def _parse_license(self, license_data: Dict[str, Any]) -> SnykLicense:
530
+ """Parse license data from Snyk response"""
531
+
532
+ return SnykLicense(
533
+ id=license_data.get("id", ""),
534
+ name=license_data.get("title", ""),
535
+ spdx_id=license_data.get("license"),
536
+ type=license_data.get("type", "unknown"),
537
+ url=license_data.get("url"),
538
+ is_deprecated=license_data.get("isDeprecated", False),
539
+ instructions=license_data.get("instructions", ""),
540
+ )
541
+
542
+ def _generate_manifest_content(
543
+ self, dependencies: Dict[str, str], ecosystem: str
544
+ ) -> str:
545
+ """Generate manifest file content for monitoring"""
546
+
547
+ if ecosystem.lower() == "pypi":
548
+ return "\n".join(
549
+ [f"{name}=={version}" for name, version in dependencies.items()]
550
+ )
551
+ elif ecosystem.lower() == "npm":
552
+ return json.dumps({"dependencies": dependencies}, indent=2)
553
+ else:
554
+ return str(dependencies)
555
+
556
+ def _is_cached(self, cache_key: str) -> bool:
557
+ """Check if result is cached and still valid"""
558
+ if cache_key not in self.cache:
559
+ return False
560
+
561
+ cached_time = self.cache[cache_key]["timestamp"]
562
+ return datetime.now() - cached_time < self.cache_ttl
563
+
564
+ def _cache_result(self, cache_key: str, result: Any) -> None:
565
+ """Cache scan result"""
566
+ self.cache[cache_key] = {"data": result, "timestamp": datetime.now()}
567
+
568
+ # Simple cache cleanup
569
+ if len(self.cache) > 100:
570
+ oldest_key = min(
571
+ self.cache.keys(), key=lambda k: self.cache[k]["timestamp"]
572
+ )
573
+ del self.cache[oldest_key]
574
+
575
+
576
+ # Global instance
577
+ snyk_integration = SnykIntegration()
578
+
579
+
580
+ async def get_snyk_security_report(
581
+ library_name: str, version: str = "latest", ecosystem: str = "pypi"
582
+ ) -> SecurityReport:
583
+ """Get security report using Snyk integration"""
584
+
585
+ try:
586
+ package_info = await snyk_integration.scan_package(
587
+ library_name, version, ecosystem
588
+ )
589
+
590
+ # Convert Snyk vulnerabilities to standard format
591
+ vulnerabilities = [
592
+ vuln.to_vulnerability() for vuln in package_info.vulnerabilities
593
+ ]
594
+
595
+ # Calculate security score based on Snyk data
596
+ critical_count = package_info.severity_counts.get("critical", 0)
597
+ high_count = package_info.severity_counts.get("high", 0)
598
+ medium_count = package_info.severity_counts.get("medium", 0)
599
+ low_count = package_info.severity_counts.get("low", 0)
600
+
601
+ security_score = max(
602
+ 0.0,
603
+ 100.0
604
+ - (
605
+ critical_count * 25 + high_count * 15 + medium_count * 5 + low_count * 1
606
+ ),
607
+ )
608
+
609
+ # Generate recommendations
610
+ recommendations = []
611
+ if critical_count > 0:
612
+ recommendations.append(
613
+ "🚨 Critical vulnerabilities found - Update immediately"
614
+ )
615
+ if package_info.vulnerabilities and any(
616
+ vuln.upgrade_path for vuln in package_info.vulnerabilities
617
+ ):
618
+ recommendations.append("📦 Security updates available - Consider upgrading")
619
+ if security_score >= 80:
620
+ recommendations.append("🛡️ Good security posture")
621
+ elif security_score >= 60:
622
+ recommendations.append("⚠️ Moderate risk - Monitor for updates")
623
+ else:
624
+ recommendations.append("🔴 High risk - Consider alternatives")
625
+
626
+ return SecurityReport(
627
+ library_name=library_name,
628
+ ecosystem=ecosystem,
629
+ scan_date=datetime.now().isoformat(),
630
+ total_vulnerabilities=len(vulnerabilities),
631
+ critical_count=critical_count,
632
+ high_count=high_count,
633
+ medium_count=medium_count,
634
+ low_count=low_count,
635
+ security_score=security_score,
636
+ recommendations=recommendations,
637
+ vulnerabilities=vulnerabilities,
638
+ latest_secure_version=None, # Would need additional API call
639
+ )
640
+
641
+ except Exception as e:
642
+ print(f"Snyk security report error: {e}", file=sys.stderr)
643
+ # Return empty report on error
644
+ return SecurityReport(
645
+ library_name=library_name,
646
+ ecosystem=ecosystem,
647
+ scan_date=datetime.now().isoformat(),
648
+ total_vulnerabilities=0,
649
+ critical_count=0,
650
+ high_count=0,
651
+ medium_count=0,
652
+ low_count=0,
653
+ security_score=50.0, # Neutral score
654
+ recommendations=["Unable to fetch security data"],
655
+ vulnerabilities=[],
656
+ latest_secure_version=None,
657
+ )