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
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.
|
|
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
|