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.
- devguard/INTEGRATION_SUMMARY.md +121 -0
- devguard/__init__.py +3 -0
- devguard/__main__.py +6 -0
- devguard/checkers/__init__.py +41 -0
- devguard/checkers/api_usage.py +523 -0
- devguard/checkers/aws_cost.py +331 -0
- devguard/checkers/aws_iam.py +284 -0
- devguard/checkers/base.py +25 -0
- devguard/checkers/container.py +137 -0
- devguard/checkers/domain.py +189 -0
- devguard/checkers/firecrawl.py +117 -0
- devguard/checkers/fly.py +225 -0
- devguard/checkers/github.py +210 -0
- devguard/checkers/npm.py +327 -0
- devguard/checkers/npm_security.py +244 -0
- devguard/checkers/redteam.py +290 -0
- devguard/checkers/secret.py +279 -0
- devguard/checkers/swarm.py +376 -0
- devguard/checkers/tailscale.py +143 -0
- devguard/checkers/tailsnitch.py +303 -0
- devguard/checkers/tavily.py +179 -0
- devguard/checkers/vercel.py +192 -0
- devguard/cli.py +1510 -0
- devguard/cli_helpers.py +189 -0
- devguard/config.py +249 -0
- devguard/core.py +293 -0
- devguard/dashboard.py +715 -0
- devguard/discovery.py +363 -0
- devguard/http_client.py +142 -0
- devguard/llm_service.py +481 -0
- devguard/mcp_server.py +259 -0
- devguard/metrics.py +144 -0
- devguard/models.py +208 -0
- devguard/reporting.py +1571 -0
- devguard/sarif.py +295 -0
- devguard/scripts/ANALYSIS_SUMMARY.md +141 -0
- devguard/scripts/README.md +221 -0
- devguard/scripts/auto_fix_recommendations.py +145 -0
- devguard/scripts/generate_npmignore.py +175 -0
- devguard/scripts/generate_security_report.py +324 -0
- devguard/scripts/prepublish_check.sh +29 -0
- devguard/scripts/redteam_npm_packages.py +1262 -0
- devguard/scripts/review_all_repos.py +300 -0
- devguard/spec.py +617 -0
- devguard/sweeps/__init__.py +23 -0
- devguard/sweeps/ai_editor_config_audit.py +697 -0
- devguard/sweeps/cargo_publish_audit.py +655 -0
- devguard/sweeps/dependency_audit.py +419 -0
- devguard/sweeps/gitignore_audit.py +336 -0
- devguard/sweeps/local_dev.py +260 -0
- devguard/sweeps/local_dirty_worktree_secrets.py +521 -0
- devguard/sweeps/project_flaudit.py +636 -0
- devguard/sweeps/public_github_secrets.py +680 -0
- devguard/sweeps/publish_audit.py +478 -0
- devguard/sweeps/ssh_key_audit.py +327 -0
- devguard/utils.py +174 -0
- devguard-0.2.0.dist-info/METADATA +225 -0
- devguard-0.2.0.dist-info/RECORD +60 -0
- devguard-0.2.0.dist-info/WHEEL +4 -0
- devguard-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Vercel deployment status checker."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from devguard.checkers.base import BaseChecker
|
|
9
|
+
from devguard.http_client import create_client, retry_with_backoff
|
|
10
|
+
from devguard.models import CheckResult, CheckStatus, DeploymentStatus
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VercelChecker(BaseChecker):
|
|
16
|
+
"""Check Vercel deployments for health status."""
|
|
17
|
+
|
|
18
|
+
check_type = "vercel"
|
|
19
|
+
|
|
20
|
+
async def check(self) -> CheckResult:
|
|
21
|
+
"""Check Vercel deployments."""
|
|
22
|
+
deployments: list[DeploymentStatus] = []
|
|
23
|
+
errors: list[str] = []
|
|
24
|
+
|
|
25
|
+
if not self.settings.vercel_token:
|
|
26
|
+
return CheckResult(
|
|
27
|
+
check_type=self.check_type,
|
|
28
|
+
success=False,
|
|
29
|
+
deployments=[],
|
|
30
|
+
errors=["Vercel token not configured"],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Handle SecretStr
|
|
34
|
+
vercel_token = self.settings.vercel_token
|
|
35
|
+
if hasattr(vercel_token, "get_secret_value"):
|
|
36
|
+
vercel_token = vercel_token.get_secret_value()
|
|
37
|
+
|
|
38
|
+
headers = {
|
|
39
|
+
"Authorization": f"Bearer {vercel_token}",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if self.settings.vercel_team_id:
|
|
43
|
+
headers["x-vercel-team-id"] = self.settings.vercel_team_id
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
async with create_client() as client:
|
|
47
|
+
# Get list of projects
|
|
48
|
+
projects = await self._get_projects(client, headers)
|
|
49
|
+
|
|
50
|
+
# Check deployments for each project
|
|
51
|
+
for project in projects:
|
|
52
|
+
try:
|
|
53
|
+
project_deployments = await self._get_project_deployments(
|
|
54
|
+
client, headers, project
|
|
55
|
+
)
|
|
56
|
+
deployments.extend(project_deployments)
|
|
57
|
+
except httpx.HTTPStatusError as e:
|
|
58
|
+
errors.append(
|
|
59
|
+
f"Error checking project {project}: "
|
|
60
|
+
f"HTTP {e.response.status_code} - {e.response.text[:100]}"
|
|
61
|
+
)
|
|
62
|
+
except httpx.RequestError as e:
|
|
63
|
+
errors.append(f"Error checking project {project}: Network error - {str(e)}")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
errors.append(f"Error checking project {project}: {str(e)}")
|
|
66
|
+
|
|
67
|
+
except httpx.RequestError as e:
|
|
68
|
+
errors.append(f"Error connecting to Vercel API: {str(e)}")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
errors.append(f"Error checking Vercel: {str(e)}")
|
|
71
|
+
|
|
72
|
+
# Vercel doesn't have a public billing API, so we don't track costs
|
|
73
|
+
return CheckResult(
|
|
74
|
+
check_type=self.check_type,
|
|
75
|
+
success=len(errors) == 0,
|
|
76
|
+
deployments=deployments,
|
|
77
|
+
errors=errors,
|
|
78
|
+
cost_metrics=[],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def _get_projects(self, client: httpx.AsyncClient, headers: dict) -> list[str]:
|
|
82
|
+
"""Get list of projects to monitor."""
|
|
83
|
+
if self.settings.vercel_projects_to_monitor:
|
|
84
|
+
return self.settings.vercel_projects_to_monitor
|
|
85
|
+
|
|
86
|
+
# Get all projects
|
|
87
|
+
try:
|
|
88
|
+
|
|
89
|
+
async def fetch_projects():
|
|
90
|
+
response = await client.get(
|
|
91
|
+
"https://api.vercel.com/v9/projects",
|
|
92
|
+
headers=headers,
|
|
93
|
+
)
|
|
94
|
+
response.raise_for_status()
|
|
95
|
+
return response
|
|
96
|
+
|
|
97
|
+
response = await retry_with_backoff(fetch_projects, max_retries=3)
|
|
98
|
+
data = response.json()
|
|
99
|
+
return [
|
|
100
|
+
project.get("name", "")
|
|
101
|
+
for project in data.get("projects", [])
|
|
102
|
+
if project.get("name")
|
|
103
|
+
]
|
|
104
|
+
except httpx.HTTPStatusError as e:
|
|
105
|
+
logger.warning(f"Failed to fetch Vercel projects: HTTP {e.response.status_code}")
|
|
106
|
+
except httpx.RequestError as e:
|
|
107
|
+
logger.warning(f"Failed to fetch Vercel projects: {str(e)}")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.warning(f"Unexpected error fetching Vercel projects: {str(e)}")
|
|
110
|
+
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
async def _get_project_deployments(
|
|
114
|
+
self, client: httpx.AsyncClient, headers: dict, project_name: str
|
|
115
|
+
) -> list[DeploymentStatus]:
|
|
116
|
+
"""Get deployments for a project."""
|
|
117
|
+
deployments: list[DeploymentStatus] = []
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
|
|
121
|
+
async def fetch_deployments():
|
|
122
|
+
response = await client.get(
|
|
123
|
+
"https://api.vercel.com/v6/deployments",
|
|
124
|
+
headers=headers,
|
|
125
|
+
params={"projectId": project_name, "limit": 10},
|
|
126
|
+
)
|
|
127
|
+
response.raise_for_status()
|
|
128
|
+
return response
|
|
129
|
+
|
|
130
|
+
response = await retry_with_backoff(fetch_deployments, max_retries=3)
|
|
131
|
+
data = response.json()
|
|
132
|
+
for deployment_data in data.get("deployments", []):
|
|
133
|
+
deployment = self._parse_deployment(deployment_data, project_name)
|
|
134
|
+
if deployment:
|
|
135
|
+
deployments.append(deployment)
|
|
136
|
+
except httpx.HTTPStatusError as e:
|
|
137
|
+
logger.warning(
|
|
138
|
+
f"Failed to fetch deployments for {project_name}: HTTP {e.response.status_code}"
|
|
139
|
+
)
|
|
140
|
+
except httpx.RequestError as e:
|
|
141
|
+
logger.warning(f"Network error fetching deployments for {project_name}: {str(e)}")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logger.warning(f"Unexpected error fetching deployments for {project_name}: {str(e)}")
|
|
144
|
+
|
|
145
|
+
return deployments
|
|
146
|
+
|
|
147
|
+
def _parse_deployment(
|
|
148
|
+
self, deployment_data: dict, project_name: str
|
|
149
|
+
) -> DeploymentStatus | None:
|
|
150
|
+
"""Parse a deployment from API response."""
|
|
151
|
+
try:
|
|
152
|
+
# Map status
|
|
153
|
+
state = deployment_data.get("state", "UNKNOWN").lower()
|
|
154
|
+
status_map = {
|
|
155
|
+
"ready": CheckStatus.HEALTHY,
|
|
156
|
+
"building": CheckStatus.UNKNOWN,
|
|
157
|
+
"error": CheckStatus.UNHEALTHY,
|
|
158
|
+
"queued": CheckStatus.UNKNOWN,
|
|
159
|
+
"canceled": CheckStatus.UNHEALTHY,
|
|
160
|
+
}
|
|
161
|
+
status = status_map.get(state, CheckStatus.UNKNOWN)
|
|
162
|
+
|
|
163
|
+
# Parse dates
|
|
164
|
+
created_at = None
|
|
165
|
+
if deployment_data.get("createdAt"):
|
|
166
|
+
created_at = datetime.fromtimestamp(deployment_data.get("createdAt") / 1000)
|
|
167
|
+
|
|
168
|
+
updated_at = None
|
|
169
|
+
if deployment_data.get("updatedAt"):
|
|
170
|
+
updated_at = datetime.fromtimestamp(deployment_data.get("updatedAt") / 1000)
|
|
171
|
+
|
|
172
|
+
url = deployment_data.get("url")
|
|
173
|
+
if url and not url.startswith("http"):
|
|
174
|
+
url = f"https://{url}"
|
|
175
|
+
|
|
176
|
+
return DeploymentStatus(
|
|
177
|
+
platform="vercel",
|
|
178
|
+
project_name=project_name,
|
|
179
|
+
deployment_id=deployment_data.get("uid", ""),
|
|
180
|
+
status=status,
|
|
181
|
+
url=url,
|
|
182
|
+
created_at=created_at,
|
|
183
|
+
updated_at=updated_at,
|
|
184
|
+
error_message=deployment_data.get("errorMessage"),
|
|
185
|
+
metadata={
|
|
186
|
+
"state": state,
|
|
187
|
+
"target": deployment_data.get("target"),
|
|
188
|
+
"type": deployment_data.get("type"),
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
except Exception:
|
|
192
|
+
return None
|