security-use 0.1.1__py3-none-any.whl → 0.2.9__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 (45) hide show
  1. security_use/__init__.py +9 -1
  2. security_use/auth/__init__.py +16 -0
  3. security_use/auth/client.py +223 -0
  4. security_use/auth/config.py +177 -0
  5. security_use/auth/oauth.py +317 -0
  6. security_use/cli.py +699 -34
  7. security_use/compliance/__init__.py +10 -0
  8. security_use/compliance/mapper.py +275 -0
  9. security_use/compliance/models.py +50 -0
  10. security_use/dependency_scanner.py +76 -30
  11. security_use/fixers/iac_fixer.py +173 -95
  12. security_use/iac/rules/azure.py +246 -0
  13. security_use/iac/rules/gcp.py +255 -0
  14. security_use/iac/rules/kubernetes.py +429 -0
  15. security_use/iac/rules/registry.py +56 -0
  16. security_use/parsers/__init__.py +18 -0
  17. security_use/parsers/base.py +2 -0
  18. security_use/parsers/composer.py +101 -0
  19. security_use/parsers/conda.py +97 -0
  20. security_use/parsers/dotnet.py +89 -0
  21. security_use/parsers/gradle.py +90 -0
  22. security_use/parsers/maven.py +108 -0
  23. security_use/parsers/npm.py +196 -0
  24. security_use/parsers/yarn.py +108 -0
  25. security_use/reporter.py +29 -1
  26. security_use/sbom/__init__.py +10 -0
  27. security_use/sbom/generator.py +340 -0
  28. security_use/sbom/models.py +40 -0
  29. security_use/scanner.py +15 -2
  30. security_use/sensor/__init__.py +125 -0
  31. security_use/sensor/alert_queue.py +207 -0
  32. security_use/sensor/config.py +217 -0
  33. security_use/sensor/dashboard_alerter.py +246 -0
  34. security_use/sensor/detector.py +415 -0
  35. security_use/sensor/endpoint_analyzer.py +339 -0
  36. security_use/sensor/middleware.py +521 -0
  37. security_use/sensor/models.py +140 -0
  38. security_use/sensor/webhook.py +227 -0
  39. security_use-0.2.9.dist-info/METADATA +531 -0
  40. security_use-0.2.9.dist-info/RECORD +60 -0
  41. security_use-0.2.9.dist-info/licenses/LICENSE +21 -0
  42. security_use-0.1.1.dist-info/METADATA +0 -92
  43. security_use-0.1.1.dist-info/RECORD +0 -30
  44. {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/WHEEL +0 -0
  45. {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,317 @@
1
+ """OAuth device authorization flow implementation."""
2
+
3
+ import time
4
+ import webbrowser
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timedelta
7
+ from typing import Optional, Callable
8
+
9
+ import httpx
10
+
11
+ from .config import OAUTH_CONFIG, AuthToken, UserInfo, AuthConfig
12
+
13
+
14
+ @dataclass
15
+ class DeviceCode:
16
+ """Device code response from OAuth server."""
17
+ device_code: str
18
+ user_code: str
19
+ verification_uri: str
20
+ verification_uri_complete: Optional[str]
21
+ expires_in: int
22
+ interval: int
23
+
24
+ @classmethod
25
+ def from_dict(cls, data: dict) -> "DeviceCode":
26
+ """Create from API response."""
27
+ # Normalize verification URLs to use security-use.dev
28
+ verification_uri = data["verification_uri"].replace(
29
+ "security-use.lovable.app", "security-use.dev"
30
+ )
31
+ verification_uri_complete = data.get("verification_uri_complete")
32
+ if verification_uri_complete:
33
+ verification_uri_complete = verification_uri_complete.replace(
34
+ "security-use.lovable.app", "security-use.dev"
35
+ )
36
+
37
+ return cls(
38
+ device_code=data["device_code"],
39
+ user_code=data["user_code"],
40
+ verification_uri=verification_uri,
41
+ verification_uri_complete=verification_uri_complete,
42
+ expires_in=data.get("expires_in", 900),
43
+ interval=data.get("interval", 5),
44
+ )
45
+
46
+
47
+ class OAuthError(Exception):
48
+ """OAuth authentication error."""
49
+ pass
50
+
51
+
52
+ class OAuthFlow:
53
+ """Handles OAuth device authorization flow."""
54
+
55
+ def __init__(self, config: Optional[AuthConfig] = None):
56
+ self.config = config or AuthConfig()
57
+ self.client_id = OAUTH_CONFIG["client_id"]
58
+ self.auth_url = OAUTH_CONFIG["auth_url"]
59
+ self.token_url = OAUTH_CONFIG["token_url"]
60
+ self.api_url = OAUTH_CONFIG["api_url"]
61
+ self.scopes = OAUTH_CONFIG["scopes"]
62
+
63
+ def request_device_code(self) -> DeviceCode:
64
+ """Request a device code to start the authorization flow.
65
+
66
+ Returns:
67
+ DeviceCode with the user code and verification URL.
68
+
69
+ Raises:
70
+ OAuthError: If the request fails.
71
+ """
72
+ try:
73
+ with httpx.Client(timeout=30.0) as client:
74
+ response = client.post(
75
+ self.auth_url,
76
+ json={
77
+ "client_id": self.client_id,
78
+ "scope": " ".join(self.scopes),
79
+ },
80
+ headers={"Accept": "application/json"},
81
+ )
82
+
83
+ if response.status_code == 404:
84
+ raise OAuthError(
85
+ "OAuth server not available. The dashboard at security-use.dev "
86
+ "may not be configured yet."
87
+ )
88
+
89
+ if response.status_code != 200:
90
+ raise OAuthError(f"Failed to request device code: {response.text}")
91
+
92
+ try:
93
+ return DeviceCode.from_dict(response.json())
94
+ except (ValueError, KeyError) as e:
95
+ raise OAuthError(
96
+ f"Invalid response from OAuth server: {e}"
97
+ )
98
+
99
+ except httpx.RequestError as e:
100
+ raise OAuthError(f"Network error connecting to {self.auth_url}: {e}")
101
+
102
+ def poll_for_token(
103
+ self,
104
+ device_code: DeviceCode,
105
+ on_status: Optional[Callable[[str], None]] = None,
106
+ ) -> AuthToken:
107
+ """Poll the token endpoint until authorization is complete.
108
+
109
+ Args:
110
+ device_code: The device code from request_device_code().
111
+ on_status: Optional callback for status updates.
112
+
113
+ Returns:
114
+ AuthToken on successful authorization.
115
+
116
+ Raises:
117
+ OAuthError: If authorization fails or times out.
118
+ """
119
+ start_time = time.time()
120
+ interval = device_code.interval
121
+
122
+ with httpx.Client(timeout=30.0) as client:
123
+ while time.time() - start_time < device_code.expires_in:
124
+ time.sleep(interval)
125
+
126
+ try:
127
+ response = client.post(
128
+ self.token_url,
129
+ json={
130
+ "client_id": self.client_id,
131
+ "device_code": device_code.device_code,
132
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
133
+ },
134
+ headers={"Accept": "application/json"},
135
+ )
136
+
137
+ data = response.json()
138
+
139
+ if response.status_code == 200:
140
+ # Success - got the token
141
+ expires_at = None
142
+ if "expires_in" in data:
143
+ expires_at = (
144
+ datetime.utcnow() + timedelta(seconds=data["expires_in"])
145
+ ).isoformat()
146
+
147
+ return AuthToken(
148
+ access_token=data["access_token"],
149
+ refresh_token=data.get("refresh_token"),
150
+ token_type=data.get("token_type", "Bearer"),
151
+ expires_at=expires_at,
152
+ scope=data.get("scope"),
153
+ )
154
+
155
+ # Handle pending/slow_down/errors
156
+ error = data.get("error", "")
157
+
158
+ if error == "authorization_pending":
159
+ if on_status:
160
+ on_status("Waiting for authorization...")
161
+ continue
162
+
163
+ elif error == "slow_down":
164
+ # Increase polling interval
165
+ interval = data.get("interval", interval + 5)
166
+ if on_status:
167
+ on_status("Slowing down polling...")
168
+ continue
169
+
170
+ elif error == "expired_token":
171
+ raise OAuthError("Device code expired. Please try again.")
172
+
173
+ elif error == "access_denied":
174
+ raise OAuthError("Authorization was denied.")
175
+
176
+ else:
177
+ raise OAuthError(f"Authorization failed: {error}")
178
+
179
+ except httpx.RequestError as e:
180
+ if on_status:
181
+ on_status(f"Network error, retrying...")
182
+ continue
183
+
184
+ raise OAuthError("Authorization timed out. Please try again.")
185
+
186
+ def get_user_info(self, token: AuthToken) -> UserInfo:
187
+ """Fetch user information using the access token.
188
+
189
+ Args:
190
+ token: The auth token.
191
+
192
+ Returns:
193
+ UserInfo with the authenticated user's details.
194
+
195
+ Raises:
196
+ OAuthError: If the request fails.
197
+ """
198
+ try:
199
+ with httpx.Client(timeout=30.0) as client:
200
+ response = client.get(
201
+ f"{self.api_url}/user-me",
202
+ headers={
203
+ "Authorization": f"{token.token_type} {token.access_token}",
204
+ "Accept": "application/json",
205
+ },
206
+ )
207
+
208
+ if response.status_code == 401:
209
+ raise OAuthError("Invalid or expired token")
210
+
211
+ if response.status_code != 200:
212
+ raise OAuthError(f"Failed to fetch user info: {response.text}")
213
+
214
+ data = response.json()
215
+ return UserInfo(
216
+ user_id=data.get("id", data.get("user_id", "unknown")),
217
+ email=data.get("email", "unknown"),
218
+ name=data.get("name"),
219
+ org_id=data.get("org_id"),
220
+ org_name=data.get("org_name"),
221
+ )
222
+
223
+ except httpx.RequestError as e:
224
+ raise OAuthError(f"Network error: {e}")
225
+
226
+ def refresh_token(self, refresh_token: str) -> AuthToken:
227
+ """Refresh an expired access token.
228
+
229
+ Args:
230
+ refresh_token: The refresh token.
231
+
232
+ Returns:
233
+ New AuthToken.
234
+
235
+ Raises:
236
+ OAuthError: If refresh fails.
237
+ """
238
+ try:
239
+ with httpx.Client(timeout=30.0) as client:
240
+ response = client.post(
241
+ self.token_url,
242
+ json={
243
+ "client_id": self.client_id,
244
+ "refresh_token": refresh_token,
245
+ "grant_type": "refresh_token",
246
+ },
247
+ headers={"Accept": "application/json"},
248
+ )
249
+
250
+ if response.status_code != 200:
251
+ raise OAuthError("Failed to refresh token")
252
+
253
+ data = response.json()
254
+ expires_at = None
255
+ if "expires_in" in data:
256
+ expires_at = (
257
+ datetime.utcnow() + timedelta(seconds=data["expires_in"])
258
+ ).isoformat()
259
+
260
+ return AuthToken(
261
+ access_token=data["access_token"],
262
+ refresh_token=data.get("refresh_token", refresh_token),
263
+ token_type=data.get("token_type", "Bearer"),
264
+ expires_at=expires_at,
265
+ scope=data.get("scope"),
266
+ )
267
+
268
+ except httpx.RequestError as e:
269
+ raise OAuthError(f"Network error: {e}")
270
+
271
+ def login(
272
+ self,
273
+ open_browser: bool = True,
274
+ on_status: Optional[Callable[[str], None]] = None,
275
+ ) -> tuple[AuthToken, UserInfo]:
276
+ """Perform the full device authorization flow.
277
+
278
+ Args:
279
+ open_browser: Whether to automatically open the browser.
280
+ on_status: Optional callback for status updates.
281
+
282
+ Returns:
283
+ Tuple of (AuthToken, UserInfo).
284
+
285
+ Raises:
286
+ OAuthError: If authentication fails.
287
+ """
288
+ # Request device code
289
+ device_code = self.request_device_code()
290
+
291
+ # Open browser if requested
292
+ verification_url = (
293
+ device_code.verification_uri_complete
294
+ or f"{device_code.verification_uri}?user_code={device_code.user_code}"
295
+ )
296
+
297
+ if open_browser:
298
+ webbrowser.open(verification_url)
299
+
300
+ # Poll for token
301
+ token = self.poll_for_token(device_code, on_status)
302
+
303
+ # Get user info
304
+ try:
305
+ user = self.get_user_info(token)
306
+ except OAuthError:
307
+ # If we can't get user info, create a placeholder
308
+ user = UserInfo(user_id="unknown", email="unknown")
309
+
310
+ # Save credentials
311
+ self.config.save_token(token, user)
312
+
313
+ return token, user
314
+
315
+ def logout(self) -> None:
316
+ """Clear stored credentials."""
317
+ self.config.clear()