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
@@ -0,0 +1,189 @@
1
+ """Shared helper functions for CLI commands."""
2
+
3
+ import asyncio
4
+ import logging
5
+
6
+ import httpx
7
+ from rich.console import Console
8
+
9
+ from devguard.config import Settings
10
+
11
+ console = Console()
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ async def test_github_token(token: str) -> tuple[bool, str]:
16
+ """Test a GitHub token.
17
+
18
+ Returns:
19
+ Tuple of (success, message)
20
+ """
21
+ try:
22
+ from github import Auth, Github
23
+
24
+ auth = Auth.Token(token)
25
+ github = Github(auth=auth)
26
+ user = github.get_user()
27
+ return True, f"Authenticated as {user.login}"
28
+ except Exception as e:
29
+ return False, f"Token invalid - {str(e)}"
30
+
31
+
32
+ async def test_vercel_token(token: str) -> tuple[bool, str]:
33
+ """Test a Vercel token.
34
+
35
+ Returns:
36
+ Tuple of (success, message)
37
+ """
38
+ try:
39
+ async with httpx.AsyncClient() as client:
40
+ response = await client.get(
41
+ "https://api.vercel.com/v2/user",
42
+ headers={"Authorization": f"Bearer {token}"},
43
+ timeout=10.0,
44
+ )
45
+ if response.status_code == 200:
46
+ data = response.json()
47
+ name = data.get("user", {}).get("name", "Unknown")
48
+ return True, f"Authenticated as {name}"
49
+ else:
50
+ return False, f"Token invalid (HTTP {response.status_code})"
51
+ except Exception as e:
52
+ return False, f"Error - {str(e)}"
53
+
54
+
55
+ async def test_fly_token(token: str) -> tuple[bool, str]:
56
+ """Test a Fly.io token.
57
+
58
+ Returns:
59
+ Tuple of (success, message)
60
+ """
61
+ try:
62
+ async with httpx.AsyncClient() as client:
63
+ response = await client.get(
64
+ "https://api.machines.dev/v1/apps",
65
+ headers={"Authorization": f"Bearer {token}"},
66
+ timeout=10.0,
67
+ )
68
+ if response.status_code == 200:
69
+ return True, "Token is valid"
70
+ else:
71
+ return False, f"Token invalid (HTTP {response.status_code})"
72
+ except Exception as e:
73
+ return False, f"Error - {str(e)}"
74
+
75
+
76
+ async def test_snyk_token(token: str) -> tuple[bool, str]:
77
+ """Test a Snyk token.
78
+
79
+ Returns:
80
+ Tuple of (success, message)
81
+ """
82
+ try:
83
+ async with httpx.AsyncClient() as client:
84
+ response = await client.get(
85
+ "https://api.snyk.io/v1/user/me",
86
+ headers={"Authorization": f"token {token}"},
87
+ timeout=10.0,
88
+ )
89
+ if response.status_code == 200:
90
+ data = response.json()
91
+ email = data.get("email", "Unknown")
92
+ return True, f"Authenticated as {email}"
93
+ else:
94
+ return False, f"Token invalid (HTTP {response.status_code})"
95
+ except Exception as e:
96
+ return False, f"Error - {str(e)}"
97
+
98
+
99
+ async def test_service_token(service: str, token: str) -> tuple[bool, str]:
100
+ """Test a token for a given service.
101
+
102
+ Args:
103
+ service: Service name (gh, github, vercel, fly, snyk)
104
+ token: Token to test
105
+
106
+ Returns:
107
+ Tuple of (success, message)
108
+ """
109
+ service = service.lower()
110
+
111
+ if service in ("gh", "github"):
112
+ return await test_github_token(token)
113
+ elif service == "vercel":
114
+ return await test_vercel_token(token)
115
+ elif service == "fly":
116
+ return await test_fly_token(token)
117
+ elif service == "snyk":
118
+ return await test_snyk_token(token)
119
+ else:
120
+ return False, f"Unknown service: {service}"
121
+
122
+
123
+ def show_auth_status(settings: Settings) -> None:
124
+ """Show authentication status for all configured services.
125
+
126
+ Args:
127
+ settings: Guardian settings
128
+ """
129
+ console.print("[bold blue]Guardian Authentication Status[/bold blue]\n")
130
+
131
+ # GitHub
132
+ if settings.github_token:
133
+ token_str = (
134
+ settings.github_token.get_secret_value()
135
+ if hasattr(settings.github_token, "get_secret_value")
136
+ else str(settings.github_token)
137
+ )
138
+ success, message = asyncio.run(test_github_token(token_str))
139
+ if success:
140
+ console.print(f"[bold green]✓[/bold green] GitHub: {message}")
141
+ else:
142
+ console.print(f"[bold red]✗[/bold red] GitHub: {message}")
143
+ else:
144
+ console.print("[yellow]○[/yellow] GitHub: Not configured")
145
+
146
+ # Vercel
147
+ if settings.vercel_token:
148
+ token_str = (
149
+ settings.vercel_token.get_secret_value()
150
+ if hasattr(settings.vercel_token, "get_secret_value")
151
+ else str(settings.vercel_token)
152
+ )
153
+ success, message = asyncio.run(test_vercel_token(token_str))
154
+ if success:
155
+ console.print(f"[bold green]✓[/bold green] Vercel: {message}")
156
+ else:
157
+ console.print(f"[bold red]✗[/bold red] Vercel: {message}")
158
+ else:
159
+ console.print("[yellow]○[/yellow] Vercel: Not configured")
160
+
161
+ # Fly.io
162
+ if settings.fly_api_token:
163
+ token_str = (
164
+ settings.fly_api_token.get_secret_value()
165
+ if hasattr(settings.fly_api_token, "get_secret_value")
166
+ else str(settings.fly_api_token)
167
+ )
168
+ success, message = asyncio.run(test_fly_token(token_str))
169
+ if success:
170
+ console.print(f"[bold green]✓[/bold green] Fly.io: {message}")
171
+ else:
172
+ console.print(f"[bold red]✗[/bold red] Fly.io: {message}")
173
+ else:
174
+ console.print("[yellow]○[/yellow] Fly.io: Not configured")
175
+
176
+ # Snyk
177
+ if settings.snyk_token:
178
+ token_str = (
179
+ settings.snyk_token.get_secret_value()
180
+ if hasattr(settings.snyk_token, "get_secret_value")
181
+ else str(settings.snyk_token)
182
+ )
183
+ success, message = asyncio.run(test_snyk_token(token_str))
184
+ if success:
185
+ console.print(f"[bold green]✓[/bold green] Snyk: {message}")
186
+ else:
187
+ console.print(f"[bold red]✗[/bold red] Snyk: {message}")
188
+ else:
189
+ console.print("[yellow]○[/yellow] Snyk: Not configured")
devguard/config.py ADDED
@@ -0,0 +1,249 @@
1
+ """Configuration management for devguard."""
2
+
3
+ from typing import Annotated
4
+
5
+ from pydantic import Field, SecretStr, field_validator
6
+ from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
7
+
8
+
9
+ class Settings(BaseSettings):
10
+ """Application settings loaded from environment variables."""
11
+
12
+ model_config = SettingsConfigDict(
13
+ env_file=(".env", "../.env"), # Load local first, then root fallback
14
+ env_file_encoding="utf-8",
15
+ case_sensitive=False,
16
+ extra="ignore",
17
+ )
18
+
19
+ # GitHub Configuration
20
+ github_token: SecretStr | None = Field(None, description="GitHub personal access token")
21
+ github_org: str | None = Field(None, description="GitHub organization name (optional)")
22
+
23
+ # Vercel Configuration
24
+ vercel_token: SecretStr | None = Field(None, description="Vercel API token")
25
+ vercel_team_id: str | None = Field(None, description="Vercel team ID (optional)")
26
+
27
+ # Fly.io Configuration
28
+ fly_api_token: SecretStr | None = Field(None, description="Fly.io API token")
29
+
30
+ # npm/Snyk Configuration
31
+ snyk_token: SecretStr | None = Field(None, description="Snyk API token (optional)")
32
+
33
+ # Monitoring Configuration
34
+ check_interval_seconds: int = Field(3600, description="Interval between checks in seconds")
35
+ alert_webhook_url: SecretStr | None = Field(None, description="Webhook URL for alerts")
36
+ alert_email: str | None = Field(None, description="Email address for alerts")
37
+ environment: str = Field("development", description="Environment mode (development/production)")
38
+
39
+ # Rate Limiting Configuration
40
+ rate_limit_per_minute: int = Field(60, description="Maximum API calls per minute per service")
41
+ rate_limit_per_hour: int = Field(1000, description="Maximum API calls per hour per service")
42
+
43
+ # Red Team Security Testing
44
+ redteam_enabled: bool = Field(
45
+ True, description="Enable red team security testing for deployments"
46
+ )
47
+
48
+ # Deep npm Package Security Analysis
49
+ npm_security_enabled: bool = Field(
50
+ False,
51
+ description="Enable deep security analysis of npm packages (secrets, obfuscation, etc.)",
52
+ )
53
+
54
+ # Dashboard Configuration
55
+ dashboard_enabled: bool = Field(False, description="Enable web dashboard")
56
+ dashboard_host: str = Field("0.0.0.0", description="Dashboard host to bind to")
57
+ dashboard_port: int = Field(8080, description="Dashboard port")
58
+ metrics_enabled: bool = Field(True, description="Enable Prometheus metrics")
59
+ metrics_port: int = Field(9090, description="Prometheus metrics port")
60
+ dashboard_api_key: SecretStr | None = Field(
61
+ None, description="API key for dashboard access (generate with openssl rand -hex 32)"
62
+ )
63
+ allowed_origins: Annotated[list[str], NoDecode] = Field(
64
+ default_factory=list,
65
+ description="Comma-separated list of allowed CORS origins",
66
+ )
67
+
68
+ # Additional Service API Keys
69
+ firecrawl_api_key: SecretStr | None = Field(None, description="Firecrawl API key")
70
+ tavily_api_key: SecretStr | None = Field(None, description="Tavily API key")
71
+ anthropic_api_key: SecretStr | None = Field(None, description="Anthropic API key")
72
+ openrouter_api_key: SecretStr | None = Field(None, description="OpenRouter API key")
73
+ openai_api_key: SecretStr | None = Field(None, description="OpenAI API key")
74
+ perplexity_api_key: SecretStr | None = Field(None, description="Perplexity API key")
75
+ groq_api_key: SecretStr | None = Field(None, description="Groq API key")
76
+
77
+ # SMTP Configuration (for email alerts)
78
+ smtp_host: str | None = Field(None, description="SMTP server hostname")
79
+ smtp_port: int = Field(587, description="SMTP server port")
80
+ smtp_user: str | None = Field(None, description="SMTP username")
81
+ smtp_password: SecretStr | None = Field(None, description="SMTP password")
82
+ smtp_from: str | None = Field(None, description="From email address")
83
+ smtp_use_tls: bool = Field(True, description="Use TLS for SMTP connection")
84
+ email_only_on_issues: bool = Field(
85
+ True, description="Only send emails when there are issues (skip 'all clear' reports)"
86
+ )
87
+ email_thread_id_file: str | None = Field(
88
+ None,
89
+ description="Path to file storing last email thread ID (default: .devguard-email-thread)",
90
+ )
91
+ email_history_file: str | None = Field(
92
+ None,
93
+ description="Path to JSON file storing email history for agent introspection (default: .devguard-email-history.json)",
94
+ )
95
+ email_llm_enabled: bool = Field(
96
+ True,
97
+ description="Enable LLM-powered email judgements (subject lines, send decisions, summaries). Defaults to True. Set to False to disable.",
98
+ )
99
+ use_smart_email: bool = Field(
100
+ True,
101
+ description="Use smart_email system (SNS) instead of direct SMTP. Provides batching, deduplication, and threading. Falls back to SMTP if smart_email unavailable.",
102
+ )
103
+ smart_email_db_path: str | None = Field(
104
+ None,
105
+ description="Path to smart_email SQLite database (default: /data/smart_email.db or SMART_EMAIL_DB env var)",
106
+ )
107
+
108
+ # Package Monitoring - use NoDecode to prevent JSON parsing,
109
+ # validator handles comma-separated strings
110
+ npm_packages_to_monitor: Annotated[list[str], NoDecode] = Field(
111
+ default_factory=list,
112
+ description="List of npm packages to monitor",
113
+ )
114
+
115
+ # Repository Monitoring
116
+ github_repos_to_monitor: Annotated[list[str], NoDecode] = Field(
117
+ default_factory=list,
118
+ description="List of GitHub repos to monitor (owner/repo format)",
119
+ )
120
+
121
+ # Deployment Monitoring
122
+ fly_apps_to_monitor: Annotated[list[str], NoDecode] = Field(
123
+ default_factory=list,
124
+ description="List of Fly.io apps to monitor",
125
+ )
126
+ vercel_projects_to_monitor: Annotated[list[str], NoDecode] = Field(
127
+ default_factory=list,
128
+ description="List of Vercel projects to monitor",
129
+ )
130
+
131
+ # Secret Scanning
132
+ secret_scan_enabled: bool = Field(True, description="Enable secret scanning of git repos")
133
+ secret_scan_paths: Annotated[list[str], NoDecode] = Field(
134
+ default_factory=list,
135
+ description="Paths to git repos to scan for secrets (default: _infra subprojects)",
136
+ )
137
+
138
+ # Container Security
139
+ container_check_enabled: bool = Field(
140
+ True, description="Enable Container/Dockerfile security checks"
141
+ )
142
+
143
+ # AWS IAM Security
144
+ aws_iam_check_enabled: bool = Field(
145
+ False, description="Enable AWS IAM security checks for satellite nodes"
146
+ )
147
+
148
+ # AWS Cost Monitoring
149
+ aws_cost_check_enabled: bool = Field(
150
+ False, description="Enable AWS cost monitoring and budget alerts"
151
+ )
152
+ aws_monthly_cost_ceiling: float = Field(
153
+ 100.0,
154
+ description="AWS monthly cost ceiling in USD (alerts when exceeded)",
155
+ )
156
+ aws_allowed_instances: Annotated[list[str], NoDecode] = Field(
157
+ default_factory=list,
158
+ description="Allowed EC2 instance names (comma-separated). Unlisted running instances trigger alerts.",
159
+ )
160
+
161
+ # Tailscale Network Health
162
+ tailscale_check_enabled: bool = Field(
163
+ False, description="Enable Tailscale mesh network health checks"
164
+ )
165
+ tailscale_expected_nodes: Annotated[list[str], NoDecode] = Field(
166
+ default_factory=list,
167
+ description="Expected Tailscale node hostnames (comma-separated)",
168
+ )
169
+
170
+ # Tailsnitch ACL Security Audit
171
+ tailsnitch_check_enabled: bool = Field(
172
+ False, description="Enable Tailsnitch security audit for Tailscale ACLs"
173
+ )
174
+ tailsnitch_binary_path: str | None = Field(
175
+ None, description="Custom path to tailsnitch binary (auto-detected if not set)"
176
+ )
177
+ tailsnitch_tailnet: str | None = Field(
178
+ None, description="Specific tailnet to audit (default: from API key)"
179
+ )
180
+ # Tailscale authentication (for Tailsnitch)
181
+ # Note: Tailsnitch supports both API key and OAuth
182
+ # API key: TSKEY or TS_API_KEY
183
+ # OAuth: TS_OAUTH_CLIENT_ID + TS_OAUTH_CLIENT_SECRET
184
+ # These are read from environment, not stored in Settings for security
185
+
186
+ # Domain/SSL Monitoring
187
+ domain_check_enabled: bool = Field(
188
+ False, description="Enable domain and SSL certificate monitoring"
189
+ )
190
+ domains_to_monitor: Annotated[list[str], NoDecode] = Field(
191
+ default_factory=list,
192
+ description="Domains to monitor for SSL/health (comma-separated, e.g. 'example.com,app.example.com')",
193
+ )
194
+
195
+ # Docker Swarm Health
196
+ swarm_check_enabled: bool = Field(
197
+ False, description="Enable Docker Swarm cluster health checks"
198
+ )
199
+ swarm_expected_nodes: Annotated[list[str], NoDecode] = Field(
200
+ default_factory=list,
201
+ description="Expected swarm node hostnames (comma-separated)",
202
+ )
203
+ swarm_critical_services: Annotated[list[str], NoDecode] = Field(
204
+ default_factory=list,
205
+ description="Critical swarm service names (comma-separated)",
206
+ )
207
+
208
+ # API Usage/Credits Monitoring
209
+ api_usage_check_enabled: bool = Field(
210
+ False, description="Enable API usage/credits monitoring for LLM providers"
211
+ )
212
+
213
+ @field_validator(
214
+ "npm_packages_to_monitor",
215
+ "github_repos_to_monitor",
216
+ "fly_apps_to_monitor",
217
+ "vercel_projects_to_monitor",
218
+ "allowed_origins",
219
+ "secret_scan_paths",
220
+ "domains_to_monitor",
221
+ "swarm_expected_nodes",
222
+ "swarm_critical_services",
223
+ "tailscale_expected_nodes",
224
+ "aws_allowed_instances",
225
+ mode="before",
226
+ )
227
+ @classmethod
228
+ def parse_comma_separated_string(cls, v: str | list[str] | None) -> list[str]:
229
+ """Parse comma-separated strings into lists."""
230
+ if v is None:
231
+ return []
232
+ if isinstance(v, str):
233
+ return [item.strip() for item in v.split(",") if item.strip()]
234
+ if isinstance(v, list):
235
+ return v
236
+ return []
237
+
238
+
239
+ def get_settings(env_file: str | None = None) -> Settings:
240
+ """Get application settings.
241
+
242
+ Args:
243
+ env_file: Optional path to an env file to load (e.g. "../.env" when
244
+ running Guardian from inside an infra repo). When not provided,
245
+ the Settings `model_config.env_file` default is used.
246
+ """
247
+ if env_file:
248
+ return Settings(_env_file=env_file) # type: ignore[call-arg]
249
+ return Settings() # type: ignore[call-arg]