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
security_use/__init__.py CHANGED
@@ -1,10 +1,16 @@
1
1
  """security-use - Security scanning tool for dependencies and Infrastructure as Code."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.2.9"
4
4
 
5
5
  from security_use.scanner import scan_dependencies, scan_iac
6
6
  from security_use.models import Vulnerability, IaCFinding, ScanResult
7
7
 
8
+ # Sensor imports (lazy-loaded for optional dependencies)
9
+ from security_use import sensor
10
+
11
+ # Auth imports
12
+ from security_use import auth
13
+
8
14
  __all__ = [
9
15
  "__version__",
10
16
  "scan_dependencies",
@@ -12,4 +18,6 @@ __all__ = [
12
18
  "Vulnerability",
13
19
  "IaCFinding",
14
20
  "ScanResult",
21
+ "sensor",
22
+ "auth",
15
23
  ]
@@ -0,0 +1,16 @@
1
+ """Authentication module for SecurityUse dashboard integration."""
2
+
3
+ from .config import AuthConfig, AuthToken, UserInfo, get_config_dir
4
+ from .oauth import OAuthFlow, OAuthError, DeviceCode
5
+ from .client import DashboardClient
6
+
7
+ __all__ = [
8
+ "AuthConfig",
9
+ "AuthToken",
10
+ "UserInfo",
11
+ "OAuthFlow",
12
+ "OAuthError",
13
+ "DeviceCode",
14
+ "DashboardClient",
15
+ "get_config_dir",
16
+ ]
@@ -0,0 +1,223 @@
1
+ """Dashboard API client for uploading scan results."""
2
+
3
+ import platform
4
+ from typing import Optional
5
+ from datetime import datetime
6
+
7
+ import httpx
8
+
9
+ from .config import OAUTH_CONFIG, AuthConfig
10
+ from .oauth import OAuthFlow, OAuthError
11
+ from security_use import __version__
12
+ from security_use.models import ScanResult
13
+
14
+
15
+ class DashboardClient:
16
+ """Client for the SecurityUse dashboard API."""
17
+
18
+ def __init__(self, config: Optional[AuthConfig] = None):
19
+ self.config = config or AuthConfig()
20
+ self.api_url = OAUTH_CONFIG["api_url"]
21
+ self.oauth = OAuthFlow(self.config)
22
+
23
+ def _get_headers(self) -> dict:
24
+ """Get request headers with authentication."""
25
+ headers = {
26
+ "Accept": "application/json",
27
+ "Content-Type": "application/json",
28
+ "User-Agent": f"security-use-cli/{__version__}",
29
+ }
30
+
31
+ token = self.config.get_access_token()
32
+ if token:
33
+ headers["Authorization"] = f"Bearer {token}"
34
+
35
+ return headers
36
+
37
+ def _ensure_authenticated(self) -> None:
38
+ """Ensure user is authenticated, refresh token if needed."""
39
+ if not self.config.is_authenticated:
40
+ raise OAuthError("Not authenticated. Run 'security-use auth login' first.")
41
+
42
+ # Try to refresh if token is expired
43
+ if self.config.token and self.config.token.is_expired():
44
+ if self.config.token.refresh_token:
45
+ try:
46
+ new_token = self.oauth.refresh_token(self.config.token.refresh_token)
47
+ self.config.save_token(new_token, self.config.user)
48
+ except OAuthError:
49
+ raise OAuthError(
50
+ "Session expired. Run 'security-use auth login' to re-authenticate."
51
+ )
52
+ else:
53
+ raise OAuthError(
54
+ "Session expired. Run 'security-use auth login' to re-authenticate."
55
+ )
56
+
57
+ def upload_scan(
58
+ self,
59
+ result: ScanResult,
60
+ scan_type: str = "deps",
61
+ repo_name: Optional[str] = None,
62
+ branch: Optional[str] = None,
63
+ commit_sha: Optional[str] = None,
64
+ ) -> dict:
65
+ """Upload scan results to the dashboard.
66
+
67
+ Args:
68
+ result: The scan result to upload.
69
+ scan_type: Type of scan (deps, sast, iac, runtime).
70
+ repo_name: Optional repository name.
71
+ branch: Optional git branch name.
72
+ commit_sha: Optional git commit SHA.
73
+
74
+ Returns:
75
+ API response with scan ID and summary.
76
+
77
+ Raises:
78
+ OAuthError: If not authenticated or upload fails.
79
+ """
80
+ self._ensure_authenticated()
81
+
82
+ # Convert vulnerabilities and IaC findings to the expected format
83
+ findings = []
84
+
85
+ for vuln in result.vulnerabilities:
86
+ findings.append({
87
+ "finding_type": "vulnerability",
88
+ "category": "deps",
89
+ "severity": vuln.severity.value,
90
+ "title": vuln.title,
91
+ "description": vuln.description or "",
92
+ "recommendation": f"Upgrade to version {vuln.fixed_version}" if vuln.fixed_version else "No fix available",
93
+ "cve_id": vuln.id, # Vulnerability ID is typically the CVE ID
94
+ "package_name": vuln.package,
95
+ "package_version": vuln.installed_version,
96
+ "fixed_version": vuln.fixed_version,
97
+ })
98
+
99
+ for finding in result.iac_findings:
100
+ findings.append({
101
+ "finding_type": "misconfiguration",
102
+ "category": "iac",
103
+ "severity": finding.severity.value,
104
+ "title": finding.title,
105
+ "description": finding.description or "",
106
+ "file_path": finding.file_path,
107
+ "line_number": finding.line_number,
108
+ "recommendation": finding.remediation or "",
109
+ })
110
+
111
+ payload = {
112
+ "scan_type": scan_type,
113
+ "status": "completed",
114
+ "findings": findings,
115
+ "metadata": {
116
+ "cli_version": __version__,
117
+ "os": platform.system().lower(),
118
+ "repo_name": repo_name,
119
+ "branch": branch,
120
+ "commit_sha": commit_sha,
121
+ },
122
+ }
123
+
124
+ try:
125
+ with httpx.Client(timeout=60.0) as client:
126
+ response = client.post(
127
+ f"{self.api_url}/scan-upload",
128
+ json=payload,
129
+ headers=self._get_headers(),
130
+ )
131
+
132
+ if response.status_code == 401:
133
+ raise OAuthError(
134
+ "Authentication failed. Run 'security-use auth login' to re-authenticate."
135
+ )
136
+
137
+ if response.status_code == 403:
138
+ raise OAuthError(
139
+ "Insufficient permissions. Token lacks scan:upload scope."
140
+ )
141
+
142
+ if response.status_code not in (200, 201):
143
+ raise OAuthError(f"Failed to upload scan: {response.text}")
144
+
145
+ return response.json()
146
+
147
+ except httpx.RequestError as e:
148
+ raise OAuthError(f"Network error: {e}")
149
+
150
+ def get_scans(
151
+ self,
152
+ project_name: Optional[str] = None,
153
+ limit: int = 10,
154
+ ) -> list[dict]:
155
+ """Get recent scans from the dashboard.
156
+
157
+ Args:
158
+ project_name: Optional filter by project name.
159
+ limit: Maximum number of scans to return.
160
+
161
+ Returns:
162
+ List of scan summaries.
163
+
164
+ Raises:
165
+ OAuthError: If not authenticated or request fails.
166
+ """
167
+ self._ensure_authenticated()
168
+
169
+ params = {"limit": limit}
170
+ if project_name:
171
+ params["project"] = project_name
172
+
173
+ try:
174
+ with httpx.Client(timeout=30.0) as client:
175
+ response = client.get(
176
+ f"{self.api_url}/v1/scans",
177
+ params=params,
178
+ headers=self._get_headers(),
179
+ )
180
+
181
+ if response.status_code == 401:
182
+ raise OAuthError(
183
+ "Authentication failed. Run 'security-use auth login' to re-authenticate."
184
+ )
185
+
186
+ if response.status_code != 200:
187
+ raise OAuthError(f"Failed to fetch scans: {response.text}")
188
+
189
+ return response.json().get("scans", [])
190
+
191
+ except httpx.RequestError as e:
192
+ raise OAuthError(f"Network error: {e}")
193
+
194
+ def get_projects(self) -> list[dict]:
195
+ """Get list of projects for the authenticated user.
196
+
197
+ Returns:
198
+ List of projects.
199
+
200
+ Raises:
201
+ OAuthError: If not authenticated or request fails.
202
+ """
203
+ self._ensure_authenticated()
204
+
205
+ try:
206
+ with httpx.Client(timeout=30.0) as client:
207
+ response = client.get(
208
+ f"{self.api_url}/v1/projects",
209
+ headers=self._get_headers(),
210
+ )
211
+
212
+ if response.status_code == 401:
213
+ raise OAuthError(
214
+ "Authentication failed. Run 'security-use auth login' to re-authenticate."
215
+ )
216
+
217
+ if response.status_code != 200:
218
+ raise OAuthError(f"Failed to fetch projects: {response.text}")
219
+
220
+ return response.json().get("projects", [])
221
+
222
+ except httpx.RequestError as e:
223
+ raise OAuthError(f"Network error: {e}")
@@ -0,0 +1,177 @@
1
+ """Authentication configuration and token storage."""
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass, asdict
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ from datetime import datetime, timedelta
9
+
10
+
11
+ # OAuth configuration
12
+ OAUTH_CONFIG = {
13
+ "client_id": "security-use-cli",
14
+ "auth_url": "https://lhirdknhtzkqynfavdao.supabase.co/functions/v1/oauth-device-code",
15
+ "token_url": "https://lhirdknhtzkqynfavdao.supabase.co/functions/v1/oauth-token",
16
+ "api_url": "https://lhirdknhtzkqynfavdao.supabase.co/functions/v1",
17
+ "scopes": ["read", "write", "scan:upload"],
18
+ }
19
+
20
+
21
+ def get_config_dir() -> Path:
22
+ """Get the configuration directory path."""
23
+ # Use XDG_CONFIG_HOME on Linux, or platform-specific defaults
24
+ if os.name == "nt": # Windows
25
+ config_dir = Path(os.environ.get("APPDATA", "~")).expanduser() / "security-use"
26
+ else: # macOS/Linux
27
+ xdg_config = os.environ.get("XDG_CONFIG_HOME")
28
+ if xdg_config:
29
+ config_dir = Path(xdg_config) / "security-use"
30
+ else:
31
+ config_dir = Path.home() / ".config" / "security-use"
32
+
33
+ config_dir.mkdir(parents=True, exist_ok=True)
34
+ return config_dir
35
+
36
+
37
+ def get_config_file() -> Path:
38
+ """Get the configuration file path."""
39
+ return get_config_dir() / "config.json"
40
+
41
+
42
+ def get_token_file() -> Path:
43
+ """Get the token file path."""
44
+ return get_config_dir() / "credentials.json"
45
+
46
+
47
+ @dataclass
48
+ class AuthToken:
49
+ """OAuth token data."""
50
+ access_token: str
51
+ refresh_token: Optional[str] = None
52
+ token_type: str = "Bearer"
53
+ expires_at: Optional[str] = None
54
+ scope: Optional[str] = None
55
+
56
+ def is_expired(self) -> bool:
57
+ """Check if the token is expired."""
58
+ if not self.expires_at:
59
+ return False
60
+ try:
61
+ expires = datetime.fromisoformat(self.expires_at)
62
+ return datetime.utcnow() >= expires
63
+ except ValueError:
64
+ return False
65
+
66
+ def to_dict(self) -> dict:
67
+ """Convert to dictionary."""
68
+ return asdict(self)
69
+
70
+ @classmethod
71
+ def from_dict(cls, data: dict) -> "AuthToken":
72
+ """Create from dictionary."""
73
+ return cls(
74
+ access_token=data["access_token"],
75
+ refresh_token=data.get("refresh_token"),
76
+ token_type=data.get("token_type", "Bearer"),
77
+ expires_at=data.get("expires_at"),
78
+ scope=data.get("scope"),
79
+ )
80
+
81
+
82
+ @dataclass
83
+ class UserInfo:
84
+ """Authenticated user information."""
85
+ user_id: str
86
+ email: str
87
+ name: Optional[str] = None
88
+ org_id: Optional[str] = None
89
+ org_name: Optional[str] = None
90
+
91
+ def to_dict(self) -> dict:
92
+ """Convert to dictionary."""
93
+ return asdict(self)
94
+
95
+ @classmethod
96
+ def from_dict(cls, data: dict) -> "UserInfo":
97
+ """Create from dictionary."""
98
+ return cls(
99
+ user_id=data["user_id"],
100
+ email=data["email"],
101
+ name=data.get("name"),
102
+ org_id=data.get("org_id"),
103
+ org_name=data.get("org_name"),
104
+ )
105
+
106
+
107
+ class AuthConfig:
108
+ """Manages authentication configuration and tokens."""
109
+
110
+ def __init__(self):
111
+ self._token: Optional[AuthToken] = None
112
+ self._user: Optional[UserInfo] = None
113
+ self._load()
114
+
115
+ def _load(self) -> None:
116
+ """Load credentials from file."""
117
+ token_file = get_token_file()
118
+ if token_file.exists():
119
+ try:
120
+ data = json.loads(token_file.read_text())
121
+ if "token" in data:
122
+ self._token = AuthToken.from_dict(data["token"])
123
+ if "user" in data:
124
+ self._user = UserInfo.from_dict(data["user"])
125
+ except (json.JSONDecodeError, KeyError):
126
+ pass
127
+
128
+ def _save(self) -> None:
129
+ """Save credentials to file."""
130
+ token_file = get_token_file()
131
+ data = {}
132
+ if self._token:
133
+ data["token"] = self._token.to_dict()
134
+ if self._user:
135
+ data["user"] = self._user.to_dict()
136
+
137
+ token_file.write_text(json.dumps(data, indent=2))
138
+
139
+ # Set restrictive permissions on token file (Unix only)
140
+ if os.name != "nt":
141
+ os.chmod(token_file, 0o600)
142
+
143
+ @property
144
+ def token(self) -> Optional[AuthToken]:
145
+ """Get the current auth token."""
146
+ return self._token
147
+
148
+ @property
149
+ def user(self) -> Optional[UserInfo]:
150
+ """Get the current user info."""
151
+ return self._user
152
+
153
+ @property
154
+ def is_authenticated(self) -> bool:
155
+ """Check if user is authenticated."""
156
+ return self._token is not None and not self._token.is_expired()
157
+
158
+ def save_token(self, token: AuthToken, user: Optional[UserInfo] = None) -> None:
159
+ """Save authentication token and user info."""
160
+ self._token = token
161
+ if user:
162
+ self._user = user
163
+ self._save()
164
+
165
+ def clear(self) -> None:
166
+ """Clear all stored credentials."""
167
+ self._token = None
168
+ self._user = None
169
+ token_file = get_token_file()
170
+ if token_file.exists():
171
+ token_file.unlink()
172
+
173
+ def get_access_token(self) -> Optional[str]:
174
+ """Get the access token if authenticated."""
175
+ if self.is_authenticated and self._token:
176
+ return self._token.access_token
177
+ return None