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.
- python_ubel-0.1.0.dist-info/METADATA +224 -0
- python_ubel-0.1.0.dist-info/RECORD +17 -0
- python_ubel-0.1.0.dist-info/WHEEL +5 -0
- python_ubel-0.1.0.dist-info/entry_points.txt +4 -0
- python_ubel-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_ubel-0.1.0.dist-info/top_level.txt +1 -0
- ubel/__init__.py +0 -0
- ubel/cli.py +141 -0
- ubel/client.py +50 -0
- ubel/cvss_parser.py +61 -0
- ubel/info.py +37 -0
- ubel/linux_runner.py +290 -0
- ubel/node_runner.py +399 -0
- ubel/policy.py +44 -0
- ubel/python_runner.py +126 -0
- ubel/ubel_engine.py +640 -0
- ubel/utils.py +32 -0
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 = " " * (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)
|