devguard 0.2.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 (60) hide show
  1. devguard/INTEGRATION_SUMMARY.md +121 -0
  2. devguard/__init__.py +3 -0
  3. devguard/__main__.py +6 -0
  4. devguard/checkers/__init__.py +41 -0
  5. devguard/checkers/api_usage.py +523 -0
  6. devguard/checkers/aws_cost.py +331 -0
  7. devguard/checkers/aws_iam.py +284 -0
  8. devguard/checkers/base.py +25 -0
  9. devguard/checkers/container.py +137 -0
  10. devguard/checkers/domain.py +189 -0
  11. devguard/checkers/firecrawl.py +117 -0
  12. devguard/checkers/fly.py +225 -0
  13. devguard/checkers/github.py +210 -0
  14. devguard/checkers/npm.py +327 -0
  15. devguard/checkers/npm_security.py +244 -0
  16. devguard/checkers/redteam.py +290 -0
  17. devguard/checkers/secret.py +279 -0
  18. devguard/checkers/swarm.py +376 -0
  19. devguard/checkers/tailscale.py +143 -0
  20. devguard/checkers/tailsnitch.py +303 -0
  21. devguard/checkers/tavily.py +179 -0
  22. devguard/checkers/vercel.py +192 -0
  23. devguard/cli.py +1510 -0
  24. devguard/cli_helpers.py +189 -0
  25. devguard/config.py +249 -0
  26. devguard/core.py +293 -0
  27. devguard/dashboard.py +715 -0
  28. devguard/discovery.py +363 -0
  29. devguard/http_client.py +142 -0
  30. devguard/llm_service.py +481 -0
  31. devguard/mcp_server.py +259 -0
  32. devguard/metrics.py +144 -0
  33. devguard/models.py +208 -0
  34. devguard/reporting.py +1571 -0
  35. devguard/sarif.py +295 -0
  36. devguard/scripts/ANALYSIS_SUMMARY.md +141 -0
  37. devguard/scripts/README.md +221 -0
  38. devguard/scripts/auto_fix_recommendations.py +145 -0
  39. devguard/scripts/generate_npmignore.py +175 -0
  40. devguard/scripts/generate_security_report.py +324 -0
  41. devguard/scripts/prepublish_check.sh +29 -0
  42. devguard/scripts/redteam_npm_packages.py +1262 -0
  43. devguard/scripts/review_all_repos.py +300 -0
  44. devguard/spec.py +617 -0
  45. devguard/sweeps/__init__.py +23 -0
  46. devguard/sweeps/ai_editor_config_audit.py +697 -0
  47. devguard/sweeps/cargo_publish_audit.py +655 -0
  48. devguard/sweeps/dependency_audit.py +419 -0
  49. devguard/sweeps/gitignore_audit.py +336 -0
  50. devguard/sweeps/local_dev.py +260 -0
  51. devguard/sweeps/local_dirty_worktree_secrets.py +521 -0
  52. devguard/sweeps/project_flaudit.py +636 -0
  53. devguard/sweeps/public_github_secrets.py +680 -0
  54. devguard/sweeps/publish_audit.py +478 -0
  55. devguard/sweeps/ssh_key_audit.py +327 -0
  56. devguard/utils.py +174 -0
  57. devguard-0.2.0.dist-info/METADATA +225 -0
  58. devguard-0.2.0.dist-info/RECORD +60 -0
  59. devguard-0.2.0.dist-info/WHEEL +4 -0
  60. devguard-0.2.0.dist-info/entry_points.txt +2 -0
devguard/mcp_server.py ADDED
@@ -0,0 +1,259 @@
1
+ """MCP Server for Guardian.
2
+
3
+ This module exposes Guardian's capabilities as a Model Context Protocol (MCP) server,
4
+ allowing AI assistants to directly run security checks and retrieve reports.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+
10
+ from mcp.server.fastmcp import FastMCP
11
+
12
+ from devguard.config import get_settings
13
+ from devguard.core import Guardian
14
+ from devguard.models import GuardianReport
15
+ from devguard.reporting import Reporter
16
+
17
+ # Initialize FastMCP server
18
+ mcp = FastMCP("devguard")
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @mcp.tool()
23
+ async def run_checks(
24
+ json_output: bool = True,
25
+ npm_packages: list[str] | None = None,
26
+ github_repos: list[str] | None = None,
27
+ ) -> str:
28
+ """Run security checks and return the report.
29
+
30
+ Args:
31
+ json_output: Whether to return JSON (default: True)
32
+ npm_packages: Optional list of npm packages to check (overrides config)
33
+ github_repos: Optional list of GitHub repos to check (overrides config)
34
+ """
35
+ # Validate inputs to prevent injection or malicious patterns
36
+ if npm_packages:
37
+ for pkg in npm_packages:
38
+ if not pkg or any(c in pkg for c in [";", "&", "|", "`", "$"]):
39
+ return json.dumps({"error": f"Invalid package name: {pkg}"})
40
+
41
+ if github_repos:
42
+ for repo in github_repos:
43
+ if not repo or not all(c.isalnum() or c in "/-._" for c in repo):
44
+ return json.dumps({"error": f"Invalid repo format: {repo}"})
45
+
46
+ settings = get_settings()
47
+
48
+ # Override settings if provided
49
+ if npm_packages:
50
+ settings.npm_packages_to_monitor = npm_packages
51
+ if github_repos:
52
+ settings.github_repos_to_monitor = github_repos
53
+
54
+ guardian = Guardian(settings)
55
+ report = await guardian.run_checks()
56
+
57
+ if json_output:
58
+ # Convert to dict for JSON serialization
59
+ reporter = Reporter(settings)
60
+ # We redact sensitive info before returning via MCP
61
+ report_dict = reporter._report_to_dict(report)
62
+ return json.dumps(report_dict, indent=2)
63
+
64
+ return _format_text_report(report)
65
+
66
+
67
+ @mcp.tool()
68
+ async def scan_npm_package(package_name: str) -> str:
69
+ """Deep scan a specific npm package for vulnerabilities.
70
+
71
+ Args:
72
+ package_name: The name of the npm package to scan
73
+ """
74
+ settings = get_settings()
75
+ # Enable deep security scanning
76
+ settings.npm_security_enabled = True
77
+ settings.npm_packages_to_monitor = [package_name]
78
+
79
+ guardian = Guardian(settings)
80
+
81
+ # Run only the npm checker using the new checker_types parameter
82
+ report = await guardian.run_checks(checker_types=["npm"])
83
+
84
+ # Get npm results (should be only one)
85
+ npm_checks = [c for c in report.checks if c.check_type == "npm"]
86
+ if not npm_checks:
87
+ return f"No results found for {package_name}"
88
+
89
+ result = npm_checks[0]
90
+ return json.dumps(
91
+ {
92
+ "success": result.success,
93
+ "vulnerabilities": [v.model_dump() for v in result.vulnerabilities],
94
+ "metadata": result.metadata,
95
+ },
96
+ indent=2,
97
+ )
98
+
99
+
100
+ @mcp.tool()
101
+ async def get_email_history(limit: int = 10) -> str:
102
+ """Get recent email alert history for introspection.
103
+
104
+ Returns JSON array of recent emails with their summaries, issues, and metadata.
105
+ If USE_SMART_EMAIL is enabled, reads from unified SQLite database (includes Guardian + agent alerts).
106
+ Otherwise, reads from Guardian's JSON history.
107
+
108
+ Useful for agents to understand email patterns and decide when/how to send alerts.
109
+
110
+ Args:
111
+ limit: Maximum number of recent emails to return (default: 10, max: 100)
112
+ """
113
+ from devguard.reporting import Reporter
114
+
115
+ settings = get_settings()
116
+ reporter = Reporter(settings)
117
+ history = reporter.get_email_history(limit=min(limit, 100))
118
+
119
+ return json.dumps(history, indent=2, default=str)
120
+
121
+
122
+ @mcp.tool()
123
+ async def get_unified_alert_history(limit: int = 20, topic: str | None = None) -> str:
124
+ """Get unified alert history from smart_email system (all agents + Guardian).
125
+
126
+ Returns alerts from all sources (Guardian, SRE Agent, Watchdog, etc.) in a unified format.
127
+ Only works if smart_email system is available.
128
+
129
+ Args:
130
+ limit: Maximum number of alerts to return (default: 20, max: 100)
131
+ topic: Optional topic filter (e.g., 'security_posture', 'cost_anomaly')
132
+ """
133
+ settings = get_settings()
134
+ use_smart_email = getattr(settings, "use_smart_email", False)
135
+
136
+ if not use_smart_email:
137
+ return json.dumps(
138
+ {
139
+ "error": "smart_email not enabled. Set USE_SMART_EMAIL=true to use unified history.",
140
+ "fallback": "Use get_email_history() for Guardian-only history",
141
+ },
142
+ indent=2,
143
+ )
144
+
145
+ try:
146
+ import sqlite3
147
+
148
+ from devguard.utils import get_smart_email_db_path, import_smart_email
149
+
150
+ smart_email = import_smart_email()
151
+ if not smart_email:
152
+ return json.dumps(
153
+ {
154
+ "error": "smart_email not enabled. Set USE_SMART_EMAIL=true to use unified history.",
155
+ "fallback": "Use get_email_history() for Guardian-only history",
156
+ },
157
+ indent=2,
158
+ )
159
+
160
+ init_db = smart_email.init_db
161
+ db_path = get_smart_email_db_path(settings)
162
+
163
+ init_db(db_path)
164
+ conn = sqlite3.connect(str(db_path))
165
+
166
+ # Query all alerts (optionally filtered by topic)
167
+ if topic:
168
+ rows = conn.execute(
169
+ """
170
+ SELECT topic, severity, subject, sent_at, author, metadata_json
171
+ FROM alert_history
172
+ WHERE topic LIKE ?
173
+ ORDER BY sent_at DESC
174
+ LIMIT ?
175
+ """,
176
+ (f"%{topic}%", min(limit, 100)),
177
+ ).fetchall()
178
+ else:
179
+ rows = conn.execute(
180
+ """
181
+ SELECT topic, severity, subject, sent_at, author, metadata_json
182
+ FROM alert_history
183
+ ORDER BY sent_at DESC
184
+ LIMIT ?
185
+ """,
186
+ (min(limit, 100),),
187
+ ).fetchall()
188
+
189
+ conn.close()
190
+
191
+ # Convert to unified format with all preserved metadata
192
+ alerts = []
193
+ for row in rows:
194
+ if len(row) >= 7: # New format with message_preview
195
+ topic_val, severity, subject, sent_at, author, message_preview, metadata_json = row
196
+ else: # Old format without message_preview
197
+ topic_val, severity, subject, sent_at, author, metadata_json = row[:6]
198
+ message_preview = ""
199
+
200
+ metadata = json.loads(metadata_json) if metadata_json else {}
201
+
202
+ # Extract all useful information for analysis
203
+ alert_entry = {
204
+ "timestamp": sent_at,
205
+ "topic": topic_val,
206
+ "severity": severity,
207
+ "subject": subject,
208
+ "author": author or "unknown",
209
+ "message_preview": message_preview,
210
+ "summary": metadata.get("summary", {}),
211
+ "issues": metadata.get("issues", {}),
212
+ "llm_decision": metadata.get("llm_decision"),
213
+ "llm_reasoning": metadata.get("llm_reasoning", {}),
214
+ "report_summary": metadata.get("report_summary", {}),
215
+ "actionable": metadata.get("actionable", False),
216
+ "context": metadata.get("context", {}), # Alert context (occurrence counts, trends)
217
+ "full_metadata": metadata, # Complete metadata for deep analysis
218
+ }
219
+
220
+ alerts.append(alert_entry)
221
+
222
+ return json.dumps({"total": len(alerts), "alerts": alerts}, indent=2, default=str)
223
+
224
+ except Exception as e:
225
+ logger.error(f"Failed to get unified alert history: {e}")
226
+ return json.dumps(
227
+ {"error": str(e), "message": "Could not read from smart_email database"}, indent=2
228
+ )
229
+
230
+
231
+ def _format_text_report(report: GuardianReport) -> str:
232
+ """Format report as text."""
233
+ lines = ["Guardian Security Report", "=" * 24, ""]
234
+
235
+ summary = report.summary
236
+ lines.append(f"Total Checks: {summary.get('total_checks', 0)}")
237
+ lines.append(f"Vulnerabilities: {summary.get('total_vulnerabilities', 0)}")
238
+ lines.append(f"Critical: {summary.get('critical_vulnerabilities', 0)}")
239
+ lines.append("")
240
+
241
+ for check in report.checks:
242
+ status = "✓" if check.success else "✗"
243
+ lines.append(f"[{status}] {check.check_type.upper()}")
244
+
245
+ for vuln in check.vulnerabilities:
246
+ lines.append(f" - [{vuln.severity}] {vuln.summary}")
247
+ if vuln.description:
248
+ lines.append(f" {vuln.description}")
249
+
250
+ if check.errors:
251
+ for error in check.errors:
252
+ lines.append(f" ! {error}")
253
+
254
+ return "\n".join(lines)
255
+
256
+
257
+ def run_mcp_server():
258
+ """Run the MCP server."""
259
+ mcp.run()
devguard/metrics.py ADDED
@@ -0,0 +1,144 @@
1
+ """Prometheus metrics exporter for Guardian."""
2
+
3
+ import logging
4
+
5
+ from prometheus_client import (
6
+ Counter,
7
+ Gauge,
8
+ Histogram,
9
+ generate_latest,
10
+ start_http_server,
11
+ )
12
+
13
+ from devguard.models import GuardianReport
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Metrics definitions
18
+ check_total = Counter(
19
+ "devguard_checks_total",
20
+ "Total number of checks performed",
21
+ ["check_type", "status"],
22
+ )
23
+
24
+ check_duration = Histogram(
25
+ "devguard_check_duration_seconds",
26
+ "Time spent performing checks",
27
+ ["check_type"],
28
+ buckets=[0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0],
29
+ )
30
+
31
+ vulnerabilities_total = Gauge(
32
+ "devguard_vulnerabilities_total",
33
+ "Total number of vulnerabilities",
34
+ ["check_type", "severity"],
35
+ )
36
+
37
+ deployments_total = Gauge(
38
+ "devguard_deployments_total",
39
+ "Total number of deployments",
40
+ ["check_type", "status"],
41
+ )
42
+
43
+ repository_alerts_total = Gauge(
44
+ "devguard_repository_alerts_total",
45
+ "Total number of repository alerts",
46
+ ["check_type", "state"],
47
+ )
48
+
49
+ # Cost metrics
50
+ service_cost = Gauge(
51
+ "devguard_service_cost_usd",
52
+ "Cost for a service in USD",
53
+ ["service", "period"],
54
+ )
55
+
56
+ service_usage = Gauge(
57
+ "devguard_service_usage",
58
+ "Usage amount for a service",
59
+ ["service", "unit"],
60
+ )
61
+
62
+ service_usage_percent = Gauge(
63
+ "devguard_service_usage_percent",
64
+ "Usage percentage for a service (0-100)",
65
+ ["service"],
66
+ )
67
+
68
+ service_usage_limit = Gauge(
69
+ "devguard_service_usage_limit",
70
+ "Usage limit for a service",
71
+ ["service", "unit"],
72
+ )
73
+
74
+ check_errors_total = Counter(
75
+ "devguard_check_errors_total",
76
+ "Total number of check errors",
77
+ ["check_type", "error_type"],
78
+ )
79
+
80
+
81
+ def update_metrics_from_report(report: GuardianReport) -> None:
82
+ """Update Prometheus metrics from a Guardian report."""
83
+ for check in report.checks:
84
+ # Update check counters
85
+ status = "success" if check.success else "failure"
86
+ check_total.labels(check_type=check.check_type, status=status).inc()
87
+
88
+ # Update vulnerability metrics
89
+ for vuln in check.vulnerabilities:
90
+ vulnerabilities_total.labels(
91
+ check_type=check.check_type, severity=vuln.severity.value
92
+ ).inc()
93
+
94
+ # Update deployment metrics
95
+ for deployment in check.deployments:
96
+ deployments_total.labels(
97
+ check_type=check.check_type, status=deployment.status.value
98
+ ).inc()
99
+
100
+ # Update repository alert metrics
101
+ for alert in check.repository_alerts:
102
+ repository_alerts_total.labels(check_type=check.check_type, state=alert.state).inc()
103
+
104
+ # Update cost metrics
105
+ for cost in check.cost_metrics:
106
+ if cost.amount is not None:
107
+ service_cost.labels(service=cost.service, period=cost.period).set(cost.amount)
108
+
109
+ if cost.usage is not None:
110
+ unit = cost.metadata.get("unit", "credits")
111
+ service_usage.labels(service=cost.service, unit=unit).set(cost.usage)
112
+
113
+ if cost.usage_percent is not None:
114
+ service_usage_percent.labels(service=cost.service).set(cost.usage_percent)
115
+
116
+ if cost.limit is not None:
117
+ unit = cost.metadata.get("unit", "credits")
118
+ service_usage_limit.labels(service=cost.service, unit=unit).set(cost.limit)
119
+
120
+ # Update error metrics
121
+ for error in check.errors:
122
+ error_type = "unknown"
123
+ if "HTTP" in error:
124
+ error_type = "http"
125
+ elif "Network" in error or "timeout" in error.lower():
126
+ error_type = "network"
127
+ elif "Authentication" in error or "Unauthorized" in error:
128
+ error_type = "auth"
129
+ check_errors_total.labels(check_type=check.check_type, error_type=error_type).inc()
130
+
131
+
132
+ def get_metrics() -> bytes:
133
+ """Get Prometheus metrics in text format."""
134
+ return generate_latest()
135
+
136
+
137
+ def start_metrics_server(port: int = 9090) -> None:
138
+ """Start Prometheus metrics HTTP server."""
139
+ try:
140
+ start_http_server(port)
141
+ logger.info(f"Prometheus metrics server started on port {port}")
142
+ except OSError as e:
143
+ logger.error(f"Failed to start metrics server on port {port}: {e}")
144
+ raise
devguard/models.py ADDED
@@ -0,0 +1,208 @@
1
+ """Data models for Guardian monitoring results."""
2
+
3
+ from datetime import UTC, datetime
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class Severity(str, Enum):
11
+ """Vulnerability severity levels."""
12
+
13
+ LOW = "low"
14
+ MEDIUM = "medium"
15
+ HIGH = "high"
16
+ CRITICAL = "critical"
17
+ WARNING = "warning" # For informational findings that aren't vulnerabilities
18
+
19
+
20
+ class CheckStatus(str, Enum):
21
+ """Status of a health check."""
22
+
23
+ HEALTHY = "healthy"
24
+ UNHEALTHY = "unhealthy"
25
+ UNKNOWN = "unknown"
26
+ DEGRADED = "degraded"
27
+
28
+
29
+ class CostMetric(BaseModel):
30
+ """Represents cost/usage metrics for a service."""
31
+
32
+ service: str # "vercel", "fly", "firecrawl", "tavily", etc.
33
+ period: str # "daily", "monthly", "billing_period"
34
+ amount: float | None = None # Cost in USD
35
+ usage: float | None = None # Usage amount (credits, requests, etc.)
36
+ limit: float | None = None # Usage limit
37
+ usage_percent: float | None = None # Usage percentage (0-100)
38
+ currency: str = "USD"
39
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
40
+ metadata: dict[str, Any] = Field(default_factory=dict)
41
+
42
+
43
+ class Vulnerability(BaseModel):
44
+ """Represents a security vulnerability."""
45
+
46
+ package_name: str
47
+ package_version: str
48
+ severity: Severity
49
+ advisory_id: str | None = None
50
+ cve_id: str | None = None
51
+ summary: str | None = None
52
+ description: str | None = None
53
+ first_patched_version: str | None = None
54
+ vulnerable_version_range: str | None = None
55
+ published_at: datetime | None = None
56
+ discovered_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
57
+ source: str # "npm", "gh", "snyk", etc.
58
+ references: list[str] = Field(default_factory=list) # URLs to documentation/advisories
59
+
60
+
61
+ class Finding(BaseModel):
62
+ """Represents a security finding (more general than a vulnerability).
63
+
64
+ Used for IAM issues, configuration problems, and other security concerns
65
+ that don't fit the package-based vulnerability model.
66
+ """
67
+
68
+ severity: Severity
69
+ title: str
70
+ description: str
71
+ resource: str # The resource being checked (role name, instance ID, etc.)
72
+ remediation: str | None = None # Suggested fix
73
+ discovered_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
74
+ metadata: dict[str, Any] = Field(default_factory=dict)
75
+
76
+
77
+ class APIUsage(BaseModel):
78
+ """API usage/credits for a service provider."""
79
+
80
+ service: str # "openrouter", "anthropic", "openai", "perplexity", "groq"
81
+ credits_total: float | None = None # Total credits purchased
82
+ credits_used: float | None = None # Credits consumed
83
+ credits_remaining: float | None = None # Credits left
84
+ usage_percent: float = 0.0 # Percentage used (0-100)
85
+ period_start: str | None = None # Start of usage period
86
+ period_end: str | None = None # End of usage period
87
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
88
+ metadata: dict[str, Any] = Field(default_factory=dict)
89
+
90
+
91
+ class DeploymentStatus(BaseModel):
92
+ """Status of a deployment."""
93
+
94
+ platform: str # "vercel", "fly"
95
+ project_name: str
96
+ deployment_id: str
97
+ status: CheckStatus
98
+ url: str | None = None
99
+ created_at: datetime | None = None
100
+ updated_at: datetime | None = None
101
+ health_check_status: CheckStatus | None = None
102
+ error_message: str | None = None
103
+ metadata: dict[str, Any] = Field(default_factory=dict)
104
+
105
+
106
+ class RepositoryAlert(BaseModel):
107
+ """Security alert from a GitHub repository."""
108
+
109
+ repository: str # "owner/repo"
110
+ alert_id: int
111
+ state: str # "open", "dismissed", "fixed"
112
+ severity: Severity
113
+ dependency: dict[str, Any]
114
+ security_advisory: dict[str, Any]
115
+ created_at: datetime
116
+ updated_at: datetime
117
+ dismissed_at: datetime | None = None
118
+ fixed_at: datetime | None = None
119
+
120
+
121
+ class CheckResult(BaseModel):
122
+ """Result of a monitoring check."""
123
+
124
+ check_type: str # "npm", "gh", "fly", "vercel", "aws_iam"
125
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
126
+ success: bool
127
+ vulnerabilities: list[Vulnerability] = Field(default_factory=list)
128
+ deployments: list[DeploymentStatus] = Field(default_factory=list)
129
+ repository_alerts: list[RepositoryAlert] = Field(default_factory=list)
130
+ findings: list[Finding] = Field(default_factory=list) # General security findings
131
+ errors: list[str] = Field(default_factory=list)
132
+ cost_metrics: list[CostMetric] = Field(default_factory=list)
133
+ api_usage: list[APIUsage] = Field(default_factory=list) # API credits/usage
134
+ metadata: dict[str, Any] = Field(default_factory=dict)
135
+
136
+
137
+ class GuardianReport(BaseModel):
138
+ """Unified report from all monitoring checks."""
139
+
140
+ generated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
141
+ checks: list[CheckResult] = Field(default_factory=list)
142
+ summary: dict[str, Any] = Field(default_factory=dict)
143
+
144
+ def get_total_vulnerabilities(self) -> int:
145
+ """Get total number of vulnerabilities across all checks."""
146
+ return sum(len(check.vulnerabilities) for check in self.checks)
147
+
148
+ def get_critical_vulnerabilities(self) -> list[Vulnerability]:
149
+ """Get all critical vulnerabilities."""
150
+ critical = []
151
+ for check in self.checks:
152
+ critical.extend([v for v in check.vulnerabilities if v.severity == Severity.CRITICAL])
153
+ return critical
154
+
155
+ def get_unhealthy_deployments(self) -> list[DeploymentStatus]:
156
+ """Get all unhealthy deployments."""
157
+ unhealthy = []
158
+ for check in self.checks:
159
+ unhealthy.extend([d for d in check.deployments if d.status != CheckStatus.HEALTHY])
160
+ return unhealthy
161
+
162
+ def get_open_repository_alerts(self) -> list[RepositoryAlert]:
163
+ """Get all open repository security alerts."""
164
+ open_alerts = []
165
+ for check in self.checks:
166
+ open_alerts.extend([a for a in check.repository_alerts if a.state == "open"])
167
+ return open_alerts
168
+
169
+ def get_total_cost(self) -> float:
170
+ """Get total cost across all services."""
171
+ total = 0.0
172
+ for check in self.checks:
173
+ for cost in check.cost_metrics:
174
+ if cost.amount is not None:
175
+ total += cost.amount
176
+ return total
177
+
178
+ def get_cost_metrics(self) -> list[CostMetric]:
179
+ """Get all cost metrics."""
180
+ metrics = []
181
+ for check in self.checks:
182
+ metrics.extend(check.cost_metrics)
183
+ return metrics
184
+
185
+ def get_total_findings(self) -> int:
186
+ """Get total number of findings across all checks."""
187
+ return sum(len(check.findings) for check in self.checks)
188
+
189
+ def get_critical_findings(self) -> list[Finding]:
190
+ """Get all critical findings."""
191
+ critical = []
192
+ for check in self.checks:
193
+ critical.extend([f for f in check.findings if f.severity == Severity.CRITICAL])
194
+ return critical
195
+
196
+ def get_high_findings(self) -> list[Finding]:
197
+ """Get all high severity findings."""
198
+ high = []
199
+ for check in self.checks:
200
+ high.extend([f for f in check.findings if f.severity == Severity.HIGH])
201
+ return high
202
+
203
+ def get_findings_by_check(self, check_type: str) -> list[Finding]:
204
+ """Get all findings from a specific check type."""
205
+ for check in self.checks:
206
+ if check.check_type == check_type:
207
+ return check.findings
208
+ return []