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.
- security_use/__init__.py +9 -1
- security_use/auth/__init__.py +16 -0
- security_use/auth/client.py +223 -0
- security_use/auth/config.py +177 -0
- security_use/auth/oauth.py +317 -0
- security_use/cli.py +699 -34
- security_use/compliance/__init__.py +10 -0
- security_use/compliance/mapper.py +275 -0
- security_use/compliance/models.py +50 -0
- security_use/dependency_scanner.py +76 -30
- security_use/fixers/iac_fixer.py +173 -95
- security_use/iac/rules/azure.py +246 -0
- security_use/iac/rules/gcp.py +255 -0
- security_use/iac/rules/kubernetes.py +429 -0
- security_use/iac/rules/registry.py +56 -0
- security_use/parsers/__init__.py +18 -0
- security_use/parsers/base.py +2 -0
- security_use/parsers/composer.py +101 -0
- security_use/parsers/conda.py +97 -0
- security_use/parsers/dotnet.py +89 -0
- security_use/parsers/gradle.py +90 -0
- security_use/parsers/maven.py +108 -0
- security_use/parsers/npm.py +196 -0
- security_use/parsers/yarn.py +108 -0
- security_use/reporter.py +29 -1
- security_use/sbom/__init__.py +10 -0
- security_use/sbom/generator.py +340 -0
- security_use/sbom/models.py +40 -0
- security_use/scanner.py +15 -2
- security_use/sensor/__init__.py +125 -0
- security_use/sensor/alert_queue.py +207 -0
- security_use/sensor/config.py +217 -0
- security_use/sensor/dashboard_alerter.py +246 -0
- security_use/sensor/detector.py +415 -0
- security_use/sensor/endpoint_analyzer.py +339 -0
- security_use/sensor/middleware.py +521 -0
- security_use/sensor/models.py +140 -0
- security_use/sensor/webhook.py +227 -0
- security_use-0.2.9.dist-info/METADATA +531 -0
- security_use-0.2.9.dist-info/RECORD +60 -0
- security_use-0.2.9.dist-info/licenses/LICENSE +21 -0
- security_use-0.1.1.dist-info/METADATA +0 -92
- security_use-0.1.1.dist-info/RECORD +0 -30
- {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/WHEEL +0 -0
- {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()
|