python-ubel 0.1.0__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.
ubel/ubel_engine.py ADDED
@@ -0,0 +1,640 @@
1
+ import requests,datetime,json,os
2
+ from concurrent.futures import ThreadPoolExecutor, as_completed
3
+ from pathlib import Path
4
+ from reportlab.platypus import (
5
+ SimpleDocTemplate,
6
+ Paragraph,
7
+ Spacer,
8
+ ListFlowable,
9
+ ListItem,
10
+ Table,
11
+ TableStyle,
12
+ PageBreak,
13
+ )
14
+ from reportlab.lib.enums import TA_CENTER
15
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
16
+ from reportlab.lib.pagesizes import A4
17
+ from reportlab.lib.units import inch
18
+ from reportlab.lib import colors
19
+ from reportlab.pdfbase.ttfonts import TTFont
20
+ from reportlab.pdfbase import pdfmetrics
21
+ import json,sys
22
+ from .policy import evaluate_policy
23
+ from .python_runner import Pypi_Manager
24
+ from .linux_runner import Linux_Manager
25
+ from .node_runner import Node_Manager
26
+ from .cvss_parser import CVSS_Parser
27
+ from .info import __version__ , __tool_name__
28
+
29
+
30
+
31
+ class Ubel_Engine:
32
+
33
+ osv_endpoint="https://api.osv.dev/v1/querybatch"
34
+
35
+ reports_location="./.ubel/local/reports"
36
+
37
+ generated_dependencies_location="./.ubel/dependencies/"
38
+
39
+ default_policy={
40
+ "infections":"block",
41
+ "severity":{
42
+ "critical":"block",
43
+ "high":"block",
44
+ "medium":"allow",
45
+ "low":"allow",
46
+ "unknown":"allow"
47
+ }
48
+ }
49
+
50
+ check_mode="health"
51
+
52
+ system_type="pypi"
53
+
54
+ engine="pip"
55
+
56
+ policy_dir="./.ubel/local/policy/"
57
+ policy_filename="config.json"
58
+
59
+ @staticmethod
60
+ def generate_requirements_file(purls):
61
+ requirements_filename="requirements.txt"
62
+ os.makedirs(Ubel_Engine.generated_dependencies_location,exist_ok=True)
63
+ requirements_file=f"{Ubel_Engine.generated_dependencies_location}/{requirements_filename}"
64
+ components=[Ubel_Engine.get_dependency_from_purl(purl) for purl in purls]
65
+ lines=[f"{comp[0]}=={comp[1]}" for comp in components]
66
+ data="\n".join(lines)
67
+ with open(requirements_file,"w") as file:
68
+ file.write(data)
69
+ file.close()
70
+ return requirements_file
71
+
72
+ @staticmethod
73
+ def load_policy():
74
+ Ubel_Engine.initiate_local_policy()
75
+ policy_file=f"{Ubel_Engine.policy_dir}/{Ubel_Engine.policy_filename}"
76
+ with open(policy_file,"r") as file:
77
+ data=json.load(file)
78
+ file.close()
79
+ return data
80
+
81
+ @staticmethod
82
+ def initiate_local_policy():
83
+ os.makedirs(Ubel_Engine.policy_dir,exist_ok=True)
84
+ policy_file=f"{Ubel_Engine.policy_dir}/{Ubel_Engine.policy_filename}"
85
+ needs_creation=False
86
+ if os.path.exists(policy_file)==False:
87
+ needs_creation=True
88
+ if needs_creation==False:
89
+ if os.path.getsize(policy_file)==0:
90
+ os.remove(policy_file)
91
+ needs_creation=True
92
+ if needs_creation==True:
93
+ with open(policy_file,"w") as file:
94
+ json.dump(Ubel_Engine.default_policy,file,indent=4)
95
+ file.close()
96
+
97
+ @staticmethod
98
+ def parse_pip_report(data):
99
+ components_list=data.get("install",[])
100
+ purls=[]
101
+ for item in components_list:
102
+ name=item["metadata"]["name"].lower()
103
+ version=item["metadata"]["version"]
104
+ purl=f"pkg:pypi/{name}@{version}"
105
+ purls.append(purl)
106
+ return purls
107
+
108
+ @staticmethod
109
+ def get_dependency_from_purl(purl:str):
110
+ info=purl.split(f"{Ubel_Engine.system_type}/")[1]
111
+ if info.count("@")!=1:
112
+ info_version=info.split("@")[-1]
113
+ info_name=info.split(f"@{info_version}")[0]
114
+ return info_name,info_version
115
+ return info.split("@")
116
+
117
+ @staticmethod
118
+ def get_inventory_from_purls(purls):
119
+ os_info=Linux_Manager.get_os_info()
120
+ inventory=[]
121
+ for purl in purls:
122
+ dep_info=Ubel_Engine.get_dependency_from_purl(purl)
123
+ item={
124
+ "id":purl,
125
+ "name":dep_info[0],
126
+ "version":dep_info[1],
127
+ "ecosystem":Ubel_Engine.system_type if Ubel_Engine.system_type!="linux" else os_info["id"],
128
+ "type":"library" if Ubel_Engine.system_type!="linux" else "application",
129
+ "state":"undetermined"
130
+ }
131
+ inventory.append(item)
132
+ return inventory
133
+
134
+ @staticmethod
135
+ def submit_to_osv(purls_list):
136
+ if purls_list==[]:
137
+ return []
138
+ page=0
139
+ page_pace=800
140
+ initial_vulnerabilities_list = []
141
+ while True:
142
+ purls=purls_list[page:page_pace+page]
143
+ page+=page_pace
144
+ if purls==[]:
145
+ break
146
+ queries=[]
147
+ for item in purls:
148
+ queries.append({ "package": { "purl": item } })
149
+ response=requests.post("https://api.osv.dev/v1/querybatch",json={"queries":queries},headers={"User-Agent": "ubel_tool"},timeout=60)
150
+ if response.status_code==200:
151
+ vulns=response.json().get("results",[])
152
+ pace=0
153
+ for item in vulns:
154
+ purl=purls[pace]
155
+ pace+=1
156
+ purl_info=Ubel_Engine.get_dependency_from_purl(purl)
157
+ dep=purl_info[0]
158
+ dep_version=purl_info[1]
159
+ for vul in item.get('vulns',[]):
160
+ initial_vulnerabilities_list.append({"purl":purl,"vulnerability_id":vul['id'],"dependency":dep,"affected_version":dep_version})
161
+ else:
162
+ print(response.json())
163
+ response.raise_for_status()
164
+ return initial_vulnerabilities_list
165
+
166
+ @staticmethod
167
+ def generate_fix(ranges,versions,package,ecosystem):
168
+ fixed_versions=[]
169
+ still_vulnerable_versions=[]
170
+ for item in ranges:
171
+ for event in item["events"]:
172
+ if "fixed" in event:
173
+ fixed_versions.append(event["fixed"])
174
+ elif "last_affected" in event:
175
+ still_vulnerable_versions.append(event["last_affected"])
176
+ if still_vulnerable_versions==[]:
177
+ still_vulnerable_versions=versions
178
+ if fixed_versions!=[]:
179
+ return f"Upgrade {package} ( {ecosystem} ) to: {" or ".join(fixed_versions)}"
180
+ elif still_vulnerable_versions!=[]:
181
+ return f"Upgrade {package} ( {ecosystem} ) to a version higher than: {" or ".join(still_vulnerable_versions)}"
182
+ return f"No fix available for {package}"
183
+
184
+ @staticmethod
185
+ def get_fix(vuln:dict):
186
+ remediations=[]
187
+ affected_info=vuln["affected"]
188
+ dependency=vuln["affected_dependency"]
189
+ for item in affected_info:
190
+ package=item.get("package",{})
191
+ ranges=item.get("ranges",[])
192
+ versions=item.get("versions",[])
193
+ if package.get("name").lower()==dependency.lower():
194
+ ecosystem=package.get("ecosystem")
195
+ remediations.append(Ubel_Engine.generate_fix(ranges,versions,package["name"],ecosystem))
196
+ vuln["fixes"]=remediations
197
+
198
+
199
+ @staticmethod
200
+ def get_vul_by_id(vuln:dict):
201
+ vuln_id=vuln["vulnerability_id"]
202
+ purl=vuln["purl"]
203
+ removable_info=["database_specific","affected","schema_version"]
204
+ url=f"https://api.osv.dev/v1/vulns/{vuln_id}"
205
+ response=requests.get(url,headers={"User-Agent": "ubel_tool"},timeout=60)
206
+ if response.status_code!=200:
207
+ return
208
+ data=response.json()
209
+ CVSS_Parser.process_vulnerability(data)
210
+ data["affected_purl"]=purl
211
+ data["affected_dependency"]=vuln["dependency"]
212
+ data["affected_dependency_version"]=vuln["affected_version"]
213
+ data["url"]=f"https://osv.dev/vulnerability/{vuln_id}"
214
+ data["is_infection"]=data["id"].startswith("MAL-")
215
+ Ubel_Engine.get_fix(data)
216
+ for item in removable_info:
217
+ if item in data:
218
+ del data[item]
219
+ return data
220
+
221
+ @staticmethod
222
+ def dict_to_str(data, indent=0, step=4):
223
+ """
224
+ Recursively pretty-print a dict (and lists) with clean indentation.
225
+ """
226
+ lines = []
227
+ pad = " " * indent
228
+
229
+ if isinstance(data, dict):
230
+ for key, value in data.items():
231
+ lines.append(f"{pad}{key}:")
232
+ if isinstance(value, (dict, list)):
233
+ lines.append(Ubel_Engine.dict_to_str(value, indent + step, step))
234
+ else:
235
+ lines.append(" " * (indent + step) + str(value))
236
+ elif isinstance(data, list):
237
+ for item in data:
238
+ if isinstance(item, (dict, list)):
239
+ lines.append(Ubel_Engine.dict_to_str(item, indent + step, step))
240
+ else:
241
+ lines.append(" " * indent + f"- {item}")
242
+ else:
243
+ lines.append(pad + str(data))
244
+
245
+ return "\n".join(lines)
246
+
247
+ @staticmethod
248
+ def set_inventory_state(infected_purls, vulnerable_purls, inventory):
249
+ for item in inventory:
250
+ state="safe"
251
+ if item.get("id") in infected_purls:
252
+ state="infected"
253
+ elif item.get("id") in vulnerable_purls:
254
+ state="vulnerable"
255
+ item["state"] = state
256
+
257
+
258
+ @staticmethod
259
+ def scan(pip_args):
260
+ # ----------------------------------
261
+ # Prepare Output Paths
262
+ # ----------------------------------
263
+ timestamp_date= datetime.datetime.now(datetime.UTC)
264
+ timestamp=timestamp_date.strftime("%Y_%m_%d__%H_%M_%S")
265
+ date_path="/".join(timestamp.split("_")[:3])
266
+ output_dir = Path(f'{Ubel_Engine.reports_location}/{Ubel_Engine.system_type}/{Ubel_Engine.check_mode}/{date_path}')
267
+ output_dir.mkdir(parents=True, exist_ok=True)
268
+
269
+ base_file_name = f"{Ubel_Engine.system_type}_{Ubel_Engine.check_mode}_{Ubel_Engine.engine}__{timestamp}"
270
+ pdf_path = output_dir / f"{base_file_name}.pdf"
271
+ json_path = output_dir / f"{base_file_name}.json"
272
+ artifact_path = output_dir / f"{base_file_name}__artifact.{Ubel_Engine.system_type}"
273
+ policy=Ubel_Engine.load_policy()
274
+ purls=[]
275
+ report_content=None
276
+ if Ubel_Engine.system_type=="pypi":
277
+ if Ubel_Engine.check_mode in ["check","install"]:
278
+ report_content = Pypi_Manager.run_dry_run(pip_args)
279
+ if isinstance(report_content, str):
280
+ report_content = json.loads(report_content)
281
+
282
+ purls = Ubel_Engine.parse_pip_report(report_content)
283
+ else:
284
+ purls=Pypi_Manager.get_installed()
285
+ packages=[Ubel_Engine.get_dependency_from_purl(purl) for purl in purls]
286
+ report_content=Pypi_Manager.get_installed_inventory()
287
+ elif Ubel_Engine.system_type=="npm":
288
+ if Ubel_Engine.check_mode in ["check","install"]:
289
+ purls = Node_Manager.run_dry_run(pip_args)
290
+ report_content = Node_Manager.current_lock_file_content
291
+ packages=[Ubel_Engine.get_dependency_from_purl(purl) for purl in purls]
292
+ else:
293
+ purls=Node_Manager.get_installed(Ubel_Engine.engine)
294
+ packages=[Ubel_Engine.get_dependency_from_purl(purl) for purl in purls]
295
+ if Ubel_Engine.engine=="npm":
296
+ with open("package-lock.json","r",encoding="utf-8") as af:
297
+ report_content=json.load(af)
298
+ af.close()
299
+ else:
300
+ if Ubel_Engine.check_mode in ["check","install"]:
301
+ packages=Linux_Manager.resolve_packages(pip_args)
302
+ system_info=Linux_Manager.get_os_info()
303
+ report_content={"packages":packages,"system_info":system_info}
304
+ purls=[Linux_Manager.package_to_purl(system_info["id"],pkg["name"],pkg["version"]) for pkg in packages]
305
+ else:
306
+ purls=Linux_Manager.get_linux_packages()
307
+ packages=Ubel_Engine.get_inventory_from_purls(purls)
308
+ system_info=Linux_Manager.get_os_info()
309
+ report_content={"packages":packages,"system_info":system_info}
310
+ vuln_ids = Ubel_Engine.submit_to_osv(purls)
311
+
312
+ purls=list(set(purls))
313
+ inventory=Ubel_Engine.get_inventory_from_purls(purls)
314
+
315
+ vulnerabilities = []
316
+ max_workers = min(40, len(vuln_ids))
317
+ if vuln_ids!=[]:
318
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
319
+ future_to_vid = {
320
+ executor.submit(Ubel_Engine.get_vul_by_id, vid): vid
321
+ for vid in vuln_ids
322
+ }
323
+
324
+ for future in as_completed(future_to_vid):
325
+ try:
326
+ v = future.result()
327
+ if v:
328
+ vulnerabilities.append(v)
329
+ except Exception as e:
330
+ # Fail-soft: do not crash entire scan because one vuln fetch failed
331
+ print(f"[!] Failed to fetch vulnerability: {e}")
332
+
333
+ # ----------------------------------
334
+ # Compute Stats
335
+ # ----------------------------------
336
+ severity_buckets = {
337
+ "critical": 0,
338
+ "high": 0,
339
+ "medium": 0,
340
+ "low": 0,
341
+ "unknown": 0,
342
+ }
343
+
344
+ infection_count = 0
345
+
346
+ vulnerable_purls = set()
347
+ infected_purls = set()
348
+
349
+ for v in vulnerabilities:
350
+ sev = (v.get("severity") or "unknown").lower()
351
+ if sev not in severity_buckets:
352
+ sev = "unknown"
353
+ severity_buckets[sev] += 1
354
+
355
+ if v.get("is_infection"):
356
+ infection_count += 1
357
+ infected_purls.add(v.get("affected_purl"))
358
+ else:
359
+ vulnerable_purls.add(v.get("affected_purl"))
360
+
361
+
362
+ Ubel_Engine.set_inventory_state(infected_purls, vulnerable_purls, inventory)
363
+
364
+ stats = {
365
+ "inventory_size": len(inventory),
366
+ "inventory_stats": {
367
+ "infected": len(infected_purls),
368
+ "vulnerable": len(vulnerable_purls),
369
+ "safe": len(inventory) - len(infected_purls) - len(vulnerable_purls),
370
+ },
371
+ "total_vulnerabilities": len(vulnerabilities),
372
+ "vulnerabilities_stats":{"severity": severity_buckets},
373
+ "total_infections": infection_count,
374
+ }
375
+
376
+
377
+ # ----------------------------------
378
+ # Save JSON
379
+ # ----------------------------------
380
+ final_json = {
381
+ "generated_at": timestamp_date.isoformat() + "Z",
382
+ "stats": stats,
383
+ "vulnerabilities": vulnerabilities,
384
+ "inventory": inventory,
385
+ "policy":policy,
386
+ }
387
+
388
+ allowed, reason = evaluate_policy(final_json)
389
+
390
+
391
+ final_json.update({"decision": {
392
+ "allowed": allowed,
393
+ "reason": reason,
394
+ }})
395
+
396
+ with open(json_path, "w", encoding="utf-8") as jf:
397
+ json.dump(final_json, jf, indent=2)
398
+
399
+ with open(artifact_path, "w", encoding="utf-8") as af:
400
+ af.write(json.dumps(report_content, indent=2))
401
+ af.close()
402
+
403
+ # ----------------------------------
404
+ # Generate PDF
405
+ # ----------------------------------
406
+ doc = SimpleDocTemplate(str(pdf_path), pagesize=A4)
407
+ elements = []
408
+
409
+ styles = getSampleStyleSheet()
410
+ title_style = styles["Heading1"]
411
+ section_style = styles["Heading2"]
412
+ normal_style = styles["Normal"]
413
+
414
+ elements.append(Paragraph(f"Local Vulnerability Report by: {__tool_name__} v{__version__}", title_style))
415
+ elements.append(Spacer(1, 0.3 * inch))
416
+
417
+ elements.append(Paragraph(f"Date: {timestamp_date.strftime('%Y-%m-%d %H:%M:%S')}", title_style))
418
+ elements.append(Spacer(1, 0.3 * inch))
419
+
420
+ elements.append(Paragraph(f"Scan Type: {Ubel_Engine.check_mode}", title_style))
421
+ elements.append(Spacer(1, 0.3 * inch))
422
+
423
+ elements.append(Paragraph(f"Scanned Ecosystem: {Ubel_Engine.system_type} ( {Ubel_Engine.engine} )", section_style))
424
+ elements.append(Spacer(1, 0.3 * inch))
425
+
426
+ elements.append(Paragraph(f"Scan Decision: {'ALLOWED' if allowed else 'BLOCKED'} - {reason}", section_style))
427
+ elements.append(Spacer(1, 0.2 * inch))
428
+ elements.append(Paragraph("Policy Details", section_style))
429
+ elements.append(Spacer(1, 0.3 * inch))
430
+
431
+ for k, v in policy.items():
432
+ if isinstance(v, dict):
433
+ elements.append(Paragraph(f"<b>{k.capitalize()}:</b>", normal_style))
434
+ sub_list = [
435
+ ListItem(Paragraph(f"{sk.capitalize()}: {sv}", normal_style))
436
+ for sk, sv in v.items()
437
+ ]
438
+ elements.append(ListFlowable(sub_list, bulletType="bullet"))
439
+ else:
440
+ elements.append(Paragraph(f"<b>{k.capitalize()}:</b> {v}", normal_style))
441
+ elements.append(Spacer(1, 0.2 * inch))
442
+ # ---------- Stats Section ----------
443
+ elements.append(Paragraph("Statistics Summary", section_style))
444
+ elements.append(Spacer(1, 0.2 * inch))
445
+
446
+ elements.append(Paragraph(
447
+ f"<b>Inventory Size:</b> {stats['inventory_size']}",
448
+ normal_style
449
+ ))
450
+ elements.append(Spacer(1, 0.2 * inch))
451
+ inventory_list = [
452
+ ListItem(Paragraph(f"{k.capitalize()}: {v}", normal_style))
453
+ for k, v in stats['inventory_stats'].items()
454
+ ]
455
+ elements.append(ListFlowable(inventory_list, bulletType="bullet"))
456
+ elements.append(Spacer(1, 0.5 * inch))
457
+ elements.append(Paragraph(
458
+ f"<b>Infections:</b> {stats['total_infections']}",
459
+ normal_style
460
+ ))
461
+ elements.append(Spacer(1, 0.2 * inch))
462
+ elements.append(Paragraph(
463
+ f"<b>Total Vulnerabilities:</b> {stats['total_vulnerabilities']}",
464
+ normal_style
465
+ ))
466
+
467
+ severity_list = [
468
+ ListItem(Paragraph(f"{k.capitalize()}: {v}", normal_style))
469
+ for k, v in severity_buckets.items()
470
+ ]
471
+ elements.append(ListFlowable(severity_list, bulletType="bullet"))
472
+ elements.append(Spacer(1, 0.5 * inch))
473
+
474
+ #elements.append(PageBreak())
475
+
476
+
477
+ # ---------- FULL JSON RENDER ----------
478
+ elements.append(Paragraph("Vulnerability Details", section_style))
479
+ elements.append(Spacer(1, 0.3 * inch))
480
+
481
+
482
+ def render_value(key, value, indent_level=0):
483
+ indent_space = "&nbsp;" * (indent_level * 4)
484
+
485
+ if isinstance(value, dict):
486
+ elements.append(
487
+ Paragraph(f"{indent_space}<b>{key}:</b>", normal_style)
488
+ )
489
+ elements.append(Spacer(1, 0.1 * inch))
490
+ for k, v in value.items():
491
+ render_value(k, v, indent_level + 1)
492
+
493
+ elif isinstance(value, list):
494
+ elements.append(
495
+ Paragraph(f"{indent_space}<b>{key}:</b>", normal_style)
496
+ )
497
+ elements.append(Spacer(1, 0.1 * inch))
498
+
499
+ for item in value:
500
+ if isinstance(item, dict):
501
+ elements.append(
502
+ Paragraph(f"{indent_space}-", normal_style)
503
+ )
504
+ for k, v in item.items():
505
+ render_value(k, v, indent_level + 2)
506
+ else:
507
+ elements.append(
508
+ Paragraph(
509
+ f"{indent_space}- {str(item)}",
510
+ normal_style
511
+ )
512
+ )
513
+ elements.append(Spacer(1, 0.1 * inch))
514
+
515
+ else:
516
+ safe_value = str(value).replace("\n", "<br/>")
517
+ elements.append(
518
+ Paragraph(
519
+ f"{indent_space}<b>{key}:</b> {safe_value}",
520
+ normal_style
521
+ )
522
+ )
523
+ elements.append(Spacer(1, 0.1 * inch))
524
+
525
+
526
+ for v in vulnerabilities:
527
+ elements.append(Spacer(1, 0.4 * inch))
528
+ elements.append(
529
+ Paragraph(
530
+ f"<b>{v.get('affected_dependency')} "
531
+ f"{v.get('affected_dependency_version')}</b>",
532
+ section_style
533
+ )
534
+ )
535
+ elements.append(Spacer(1, 0.2 * inch))
536
+
537
+ for key, value in v.items():
538
+ render_value(key, value)
539
+
540
+ elements.append(Spacer(1, 0.5 * inch))
541
+
542
+ # ----------------------------------
543
+ # Inventory Table Section
544
+ # ----------------------------------
545
+
546
+ elements.append(PageBreak())
547
+ elements.append(Paragraph("Inventory Table", section_style))
548
+ elements.append(Spacer(1, 0.3 * inch))
549
+
550
+ # Paragraph style for wrapped and centered text
551
+ cell_style = ParagraphStyle(
552
+ "cell_style",
553
+ fontName="Helvetica",
554
+ fontSize=8,
555
+ leading=10,
556
+ alignment=TA_CENTER, # horizontal center
557
+ )
558
+
559
+ # Header row
560
+ inventory_data = [
561
+ [
562
+ Paragraph("ID", cell_style),
563
+ Paragraph("Name", cell_style),
564
+ Paragraph("Version", cell_style),
565
+ Paragraph("Ecosystem", cell_style),
566
+ Paragraph("Type", cell_style),
567
+ Paragraph("State", cell_style),
568
+ ]
569
+ ]
570
+
571
+ # Convert each field into a wrapped centered Paragraph
572
+ for v in inventory:
573
+ inventory_data.append([
574
+ Paragraph(str(v.get("id", "")), cell_style),
575
+ Paragraph(str(v.get("name", "")), cell_style),
576
+ Paragraph(str(v.get("version", "")), cell_style),
577
+ Paragraph(str(v.get("ecosystem", "")), cell_style),
578
+ Paragraph(str(v.get("type", "")), cell_style),
579
+ Paragraph(str(v.get("state", "")), cell_style),
580
+ ])
581
+
582
+ # Set column widths — prevents overflow + enforces clean layout
583
+ col_widths = [50, 120, 60, 80, 70, 60]
584
+
585
+ table = Table(inventory_data, colWidths=col_widths, repeatRows=1)
586
+
587
+ table.setStyle(TableStyle([
588
+ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#e6e6e6")),
589
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.black),
590
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
591
+ ("FONTSIZE", (0, 0), (-1, -1), 8),
592
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 6),
593
+
594
+ # Center vertically
595
+ ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
596
+
597
+ # Center alignment (Paragraph handles internal text centering)
598
+ ("ALIGN", (0, 0), (-1, -1), "CENTER"),
599
+
600
+ ("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
601
+ ]))
602
+
603
+ elements.append(table)
604
+ elements.append(Spacer(1, 0.5 * inch))
605
+
606
+ doc.build(elements)
607
+ print()
608
+ print("Policy:")
609
+ print()
610
+ print(Ubel_Engine.dict_to_str(policy))
611
+ print()
612
+ print()
613
+ print("Findings:")
614
+ print()
615
+ print(Ubel_Engine.dict_to_str(final_json["stats"]))
616
+ print()
617
+ print()
618
+ print(f"Policy Decision: {'ALLOW' if allowed else 'BLOCK'}")
619
+ print()
620
+ print()
621
+ print(f"PDF report saved to: {pdf_path}")
622
+ print(f"JSON report saved to: {json_path}")
623
+ print(f"Scan artifact saved to: {artifact_path}")
624
+ print()
625
+ print()
626
+ if not allowed:
627
+ print(f"[!] {reason}")
628
+ sys.exit(1)
629
+ if Ubel_Engine.check_mode in ["health","check"]:
630
+ sys.exit(0)
631
+ print("[+] Policy passed. Installing dependencies...")
632
+ if Ubel_Engine.system_type=="pypi":
633
+ file_path=Ubel_Engine.generate_requirements_file(purls)
634
+ Pypi_Manager.run_real_install(file_path,Ubel_Engine.engine)
635
+ elif Ubel_Engine.system_type=="npm":
636
+ packages=[Ubel_Engine.get_dependency_from_purl(purl) for purl in purls]
637
+ Node_Manager.run_real_install(packages,Ubel_Engine.engine)
638
+ else:
639
+ packages=[Ubel_Engine.get_dependency_from_purl(purl) for purl in purls]
640
+ Linux_Manager.run_real_install(packages)
ubel/utils.py ADDED
@@ -0,0 +1,32 @@
1
+ import os
2
+ from pathlib import Path
3
+ from datetime import datetime
4
+ from dotenv import load_dotenv
5
+ import requests
6
+
7
+
8
+ def load_environment():
9
+ load_dotenv()
10
+
11
+ api_key = os.getenv("UBEL_API_KEY")
12
+ asset_id = os.getenv("UBEL_ASSET_ID")
13
+ endpoint = os.getenv("UBEL_ENDPOINT")
14
+
15
+ return api_key, asset_id, endpoint
16
+
17
+
18
+ def create_output_dir(default="./"):
19
+ timestamp = datetime.now(datetime.UTC).strftime("%Y%m%d_%H%M%S")
20
+ base = Path(default+".ubel/reports/remote") / timestamp
21
+ base.mkdir(parents=True, exist_ok=True)
22
+ return base
23
+
24
+
25
+ def download_file(url: str, destination: Path):
26
+ r = requests.get(url, stream=True, timeout=300)
27
+ r.raise_for_status()
28
+
29
+ with open(destination, "wb") as f:
30
+ for chunk in r.iter_content(chunk_size=8192):
31
+ if chunk:
32
+ f.write(chunk)