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
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 []
|