ds-server-lib 6.0.0b4__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.
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: ds-server-lib
3
+ Version: 6.0.0b4
4
+ Summary: Server library for owasp depscan
5
+ Author-email: Team AppThreat <cloud@appthreat.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/owasp-dep-scan/dep-scan
8
+ Project-URL: Bug-Tracker, https://github.com/owasp-dep-scan/dep-scan/issues
9
+ Project-URL: Funding, https://owasp.org/donate/?reponame=www-project-dep-scan&title=OWASP+depscan
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: appthreat-vulnerability-db>=6.4.4
23
+ Requires-Dist: custom-json-diff>=2.1.6
24
+ Requires-Dist: quart>=0.20.0
25
+ Requires-Dist: rich>=13.9.4
26
+ Requires-Dist: ds-analysis-lib
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.3.4; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=1.2.0; extra == "dev"
30
+ Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
@@ -0,0 +1,6 @@
1
+ server_lib/__init__.py,sha256=ws_ZqTtj9Gz593MUBx4bS3xPaFsPCZcq5Be4Aee-_XE,685
2
+ server_lib/simple.py,sha256=mtP2HT927EqqkhHfb7YWryRLzh2sX8S6WRJn-7MCOZE,11380
3
+ ds_server_lib-6.0.0b4.dist-info/METADATA,sha256=Xev5RiLR16P9JVyUlK6O2UEa5BISW8KWQ3VpKdFRWQM,1290
4
+ ds_server_lib-6.0.0b4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
+ ds_server_lib-6.0.0b4.dist-info/top_level.txt,sha256=ckgyBsWNEmm2AP_zSy5Qb0ktxCsRt9jSiPwC897x9Po,11
6
+ ds_server_lib-6.0.0b4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ server_lib
server_lib/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass
2
+ from typing import Callable, Dict, List, Optional
3
+ from logging import Logger
4
+ from rich.console import Console
5
+
6
+
7
+ @dataclass
8
+ class ServerOptions:
9
+ server_host: str = "127.0.0.1"
10
+ server_port: int = 7070
11
+ cdxgen_server: Optional[str] = None
12
+ allowed_hosts: Optional[List[str]] = None
13
+ allowed_paths: Optional[List[str]] = None
14
+ console: Optional[Console] = None
15
+ logger: Optional[Logger] = None
16
+ ca_certs: Optional[str] = None
17
+ certfile: Optional[str] = None
18
+ keyfile: Optional[str] = None
19
+ debug: bool = False
20
+ max_content_length: int = 100 * 1024 * 1024 # 100MB
21
+ # Hack
22
+ create_bom: Optional[Callable] = None
server_lib/simple.py ADDED
@@ -0,0 +1,316 @@
1
+ import os
2
+
3
+ from quart import request, Quart
4
+ import tempfile
5
+
6
+ from analysis_lib import VdrAnalysisKV
7
+ from analysis_lib.vdr import VDRAnalyzer
8
+ from server_lib import ServerOptions
9
+
10
+ app = Quart(f"dep-scan server ({__name__})", static_folder=None)
11
+ app.config.from_prefixed_env(prefix="DEPSCAN_SERVER")
12
+ app.config["PROVIDE_AUTOMATIC_OPTIONS"] = True
13
+
14
+
15
+ def get_allowed_git_schemes(default_schemes=None):
16
+ if default_schemes is None:
17
+ default_schemes = {"http", "https", "git", "git+http", "git+https"}
18
+ env_var_value = os.getenv("DEPSCAN_SERVER_ALLOWED_GIT_SCHEMES")
19
+ if env_var_value is not None:
20
+ return {scheme.strip() for scheme in env_var_value.split(",") if scheme.strip()}
21
+ return default_schemes
22
+
23
+
24
+ allowed_git_schemes = get_allowed_git_schemes()
25
+
26
+
27
+ @app.get("/")
28
+ async def index():
29
+ """
30
+ :return: An empty dictionary
31
+ """
32
+ return {}
33
+
34
+
35
+ @app.before_request
36
+ async def enforce_allowlists():
37
+ LOG = app.config.get("LOGGER_INSTANCE")
38
+ is_testing = bool(os.getenv("PYTEST_CURRENT_TEST"))
39
+ if is_testing:
40
+ client_host = request.headers.get("X-Test-Remote-Addr")
41
+ else:
42
+ client_host = request.remote_addr
43
+ allowed_hosts = app.config.get("ALLOWED_HOSTS")
44
+ if allowed_hosts is not None:
45
+ if not client_host or client_host not in allowed_hosts:
46
+ if LOG:
47
+ LOG.warning(f"Blocked request from unauthorized host: {client_host}")
48
+ return {"error": "true", "message": "Host not allowed"}, 403
49
+ if request.path == "/scan":
50
+ allowed_paths = app.config.get("ALLOWED_PATHS")
51
+ if allowed_paths is not None:
52
+ path = None
53
+ if request.args.get("path"):
54
+ path = request.args.get("path")
55
+ elif request.method == "POST":
56
+ json_data = await request.get_json(silent=True)
57
+ if json_data and "path" in json_data:
58
+ path = json_data["path"]
59
+ if path:
60
+ try:
61
+ real_path = os.path.realpath(path)
62
+ if not any(
63
+ real_path.startswith(os.path.realpath(a)) for a in allowed_paths
64
+ ):
65
+ if LOG:
66
+ LOG.warning(f"Blocked request for path: {path}")
67
+ return {"error": "true", "message": "Path not allowed"}, 403
68
+ except (OSError, ValueError):
69
+ return {"error": "true", "message": "Invalid path"}, 403
70
+
71
+
72
+ @app.after_request
73
+ async def add_security_headers(response):
74
+ response.headers["X-Content-Type-Options"] = "nosniff"
75
+ response.headers["X-Frame-Options"] = "SAMEORIGIN"
76
+ response.headers["X-XSS-Protection"] = "1; mode=block"
77
+ response.headers["Strict-Transport-Security"] = (
78
+ "max-age=31536000; includeSubDomains"
79
+ )
80
+ return response
81
+
82
+
83
+ @app.route("/scan", methods=["GET", "POST"])
84
+ async def run_scan():
85
+ """
86
+ :return: A JSON response containing the SBOM file path and a list of
87
+ vulnerabilities found in the scanned packages
88
+ """
89
+ LOG = app.config.get("LOGGER_INSTANCE")
90
+ q = request.args
91
+ params = await request.get_json()
92
+ uploaded_bom_file = await request.files
93
+ create_bom = app.config.get("create_bom")
94
+ url = None
95
+ path = None
96
+ multi_project = None
97
+ project_type = None
98
+ results = []
99
+ profile = "generic"
100
+ deep = False
101
+ suggest_mode = True if q.get("suggest") in ("true", "1") else False
102
+ fuzzy_search = True if q.get("fuzzy_search") in ("true", "1") else False
103
+ if q.get("url"):
104
+ url = q.get("url")
105
+ if q.get("path"):
106
+ path = q.get("path")
107
+ if q.get("multiProject"):
108
+ multi_project = q.get("multiProject", "").lower() in ("true", "1")
109
+ if q.get("deep"):
110
+ deep = q.get("deep", "").lower() in ("true", "1")
111
+ if q.get("type"):
112
+ project_type = q.get("type")
113
+ if q.get("profile"):
114
+ profile = q.get("profile")
115
+ if params is not None:
116
+ if not url and params.get("url"):
117
+ url = params.get("url")
118
+ if not path and params.get("path"):
119
+ path = params.get("path")
120
+ if not multi_project and params.get("multiProject"):
121
+ multi_project = params.get("multiProject", "").lower() in (
122
+ "true",
123
+ "1",
124
+ )
125
+ if not deep and params.get("deep"):
126
+ deep = params.get("deep", "").lower() in (
127
+ "true",
128
+ "1",
129
+ )
130
+ if not project_type and params.get("type"):
131
+ project_type = params.get("type")
132
+ if not profile and params.get("profile"):
133
+ profile = params.get("profile")
134
+
135
+ if not path and not url and (uploaded_bom_file.get("file", None) is None):
136
+ return {
137
+ "error": "true",
138
+ "message": "path or url or a bom file upload is required",
139
+ }, 400
140
+ if not project_type:
141
+ return {"error": "true", "message": "project type is required"}, 400
142
+ cdxgen_server = app.config.get("CDXGEN_SERVER_URL")
143
+ bom_file_path = None
144
+ tmp_bom_file = None
145
+ if uploaded_bom_file.get("file", None) is not None:
146
+ bom_file = uploaded_bom_file["file"]
147
+ bom_file_suffix = str(bom_file.filename).rsplit(".", maxsplit=1)[-1]
148
+ if bom_file_suffix not in (".json", ".cdx", ".bom"):
149
+ return (
150
+ {
151
+ "error": "true",
152
+ "message": "The uploaded file must be a valid JSON.",
153
+ },
154
+ 400,
155
+ {"Content-Type": "application/json"},
156
+ )
157
+ bom_file_content = bom_file.read().decode("utf-8")
158
+ try:
159
+ _ = json.loads(bom_file_content)
160
+ if (
161
+ not isinstance(bom_data, dict)
162
+ or bom_data.get("bomFormat") != "CycloneDX"
163
+ ):
164
+ return {
165
+ "error": "true",
166
+ "message": "Uploaded file is not a valid CycloneDX BOM.",
167
+ }, 400
168
+ except (json.JSONDecodeError, KeyError):
169
+ return (
170
+ {
171
+ "error": "true",
172
+ "message": "The uploaded file must be a valid JSON.",
173
+ },
174
+ 400,
175
+ {"Content-Type": "application/json"},
176
+ )
177
+ if LOG:
178
+ LOG.debug("Processing uploaded file")
179
+ tmp_bom_file = tempfile.NamedTemporaryFile(
180
+ delete=False, suffix=f".bom.{bom_file_suffix}"
181
+ )
182
+ path = tmp_bom_file.name
183
+ file_write(path, bom_file_content)
184
+ if url:
185
+ parsed = urlparse(url)
186
+ if not parsed.scheme or parsed.scheme not in allowed_git_schemes:
187
+ return {"error": "true", "message": "URL scheme is not allowed."}, 400
188
+ # Path points to a project directory
189
+ # Bug# 233. Path could be a url
190
+ if url or (path and os.path.isdir(path)):
191
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".bom.json") as bfp:
192
+ project_type_list = project_type.split(",")
193
+ if create_bom:
194
+ bom_status = create_bom(
195
+ bfp.name,
196
+ path,
197
+ {
198
+ "url": url,
199
+ "path": path,
200
+ "project_type": project_type_list,
201
+ "multiProject": multi_project,
202
+ "cdxgen_server": cdxgen_server,
203
+ "profile": profile,
204
+ "deep": deep,
205
+ },
206
+ )
207
+ if bom_status:
208
+ if LOG:
209
+ LOG.debug("BOM file was generated successfully at %s", bfp.name)
210
+ bom_file_path = bfp.name
211
+
212
+ # Path points to a SBOM file
213
+ else:
214
+ if os.path.exists(path):
215
+ bom_file_path = path
216
+ # Direct purl-based lookups are not supported yet.
217
+ if bom_file_path is not None:
218
+ pkg_list, _ = get_pkg_list(bom_file_path)
219
+ # Here we are assuming there will be only one type
220
+ if project_type in type_audit_map:
221
+ audit_results = audit(project_type, pkg_list)
222
+ if audit_results:
223
+ results = results + audit_results
224
+ if not pkg_list:
225
+ if LOG:
226
+ LOG.debug("Empty package search attempted!")
227
+ else:
228
+ if LOG:
229
+ LOG.debug("Scanning %d oss dependencies for issues", len(pkg_list))
230
+ bom_data = json_load(bom_file_path)
231
+ if not bom_data:
232
+ cleanup_temp(tmp_bom_file)
233
+ return (
234
+ {
235
+ "error": "true",
236
+ "message": "Unable to generate SBOM. Check your input path or url.",
237
+ },
238
+ 400,
239
+ {"Content-Type": "application/json"},
240
+ )
241
+ options = VdrAnalysisKV(
242
+ project_type,
243
+ results,
244
+ pkg_aliases={},
245
+ purl_aliases={},
246
+ suggest_mode=suggest_mode,
247
+ scoped_pkgs={},
248
+ no_vuln_table=True,
249
+ bom_file=bom_file_path,
250
+ pkg_list=[],
251
+ direct_purls={},
252
+ reached_purls={},
253
+ console=console,
254
+ logger=LOG,
255
+ fuzzy_search=fuzzy_search,
256
+ )
257
+ vdr_result = VDRAnalyzer(vdr_options=options).process()
258
+ if vdr_result.success:
259
+ pkg_vulnerabilities = vdr_result.pkg_vulnerabilities
260
+ if pkg_vulnerabilities:
261
+ bom_data["vulnerabilities"] = pkg_vulnerabilities
262
+ cleanup_temp(tmp_bom_file)
263
+ return json.dumps(bom_data), 200, {"Content-Type": "application/json"}
264
+ cleanup_temp(tmp_bom_file)
265
+ return (
266
+ {
267
+ "error": "true",
268
+ "message": "Unable to generate SBOM. Check your input path or url.",
269
+ },
270
+ 500,
271
+ {"Content-Type": "application/json"},
272
+ )
273
+
274
+
275
+ def cleanup_temp(tmp_bom_file):
276
+ if tmp_bom_file:
277
+ os.remove(tmp_bom_file)
278
+
279
+
280
+ def run_server(options: ServerOptions):
281
+ app.config["CDXGEN_SERVER_URL"] = options.cdxgen_server
282
+ app.config["LOGGER_INSTANCE"] = options.logger
283
+ if options.allowed_hosts:
284
+ app.config["ALLOWED_HOSTS"] = [h.strip() for h in options.allowed_hosts if h]
285
+ if options.allowed_paths:
286
+ app.config["ALLOWED_PATHS"] = [
287
+ os.path.realpath(p) for p in options.allowed_paths if p
288
+ ]
289
+ if options.max_content_length:
290
+ app.config["MAX_CONTENT_LENGTH"] = options.max_content_length
291
+ # Dirty hack to get access to the create_bom function
292
+ if options.create_bom:
293
+ app.config["create_bom"] = create_bom
294
+ logger = options.logger
295
+ if logger:
296
+ logger.info(
297
+ f"dep-scan server running on {options.server_host}:{options.server_port}"
298
+ )
299
+ if options.server_host not in (
300
+ "127.0.0.1",
301
+ "0000:0000:0000:0000:0000:0000:0000:0001",
302
+ "0:0:0:0:0:0:0:1",
303
+ "::1",
304
+ ):
305
+ logger.warning(
306
+ "Server listening on non-local host without built-in authentication."
307
+ )
308
+ app.run(
309
+ host=options.server_host,
310
+ port=options.server_port,
311
+ debug=options.debug,
312
+ use_reloader=False,
313
+ ca_certs=options.ca_certs,
314
+ certfile=options.certfile,
315
+ keyfile=options.keyfile,
316
+ )