-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathSanePdfReport.py
253 lines (214 loc) · 10.5 KB
/
SanePdfReport.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
import demistomock as demisto
from CommonServerPython import *
import shutil
import traceback
import os
import shlex
import base64
import subprocess
from pathlib import Path
import threading
import time
import http
import tempfile
from http.server import HTTPServer
def find_unused_port() -> int: # pragma: no cover
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.bind(('localhost', 0)) # tries to bind any available port on the os
return sock.getsockname()[1]
except Exception:
start_port, end_port = 10000, 30000
for port in range(start_port, end_port + 1):
is_connection_success = sock.connect_ex(('localhost', port))
if is_connection_success == 0:
demisto.debug(f'Port {port} is already used')
else:
demisto.debug(f'Port {port} is free')
return port
raise RuntimeError("Could not find available ports")
finally:
sock.close()
WORKING_DIR = Path("/app")
DISABLE_LOGOS = True # Bugfix before sane-reports can work with image files.
MD_IMAGE_PATH = '/markdown/image'
MD_HTTP_PORT = find_unused_port()
SERVER_OBJECT = None
MD_IMAGE_SUPPORT_MIN_VER = '6.5'
TABLE_TEXT_MAX_LENGTH_SUPPORT_MIN_VER = '7.0'
TENANT_ACCOUNT_NAME = get_tenant_account_name()
MD_IMAGE_PATH_SAAS = '/xsoar/markdown/image'
MD_IMAGE_SAAS_VERSION = '8.0'
def find_zombie_processes():
"""find zombie proceses
Returns:
([process ids], raw ps output) -- return a tuple of zombie process ids and raw ps output
"""
ps_out = subprocess.check_output(['ps', '-e', '-o', 'pid,ppid,state,stime,cmd'],
stderr=subprocess.STDOUT, universal_newlines=True)
lines = ps_out.splitlines()
pid = str(os.getpid())
zombies = []
if len(lines) > 1:
for line in lines[1:]:
pinfo = line.split()
if pinfo[2] == 'Z' and pinfo[1] == pid: # zombie process
zombies.append(pinfo[0])
return zombies, ps_out
def quit_driver_and_reap_children(killMarkdownServer): # pragma: no cover
try:
if killMarkdownServer:
# Kill Markdown artifacts server
global SERVER_OBJECT
if SERVER_OBJECT:
demisto.debug("Shutting down markdown artifacts server")
SERVER_OBJECT.shutdown()
zombies, ps_out = find_zombie_processes()
if zombies: # pragma no cover
demisto.info(f'Found zombie processes will waitpid: {ps_out}')
for pid in zombies:
waitres = os.waitpid(int(pid), os.WNOHANG)[1]
demisto.info(f'waitpid result: {waitres}')
else:
demisto.debug(f'No zombie processes found for ps output: {ps_out}')
except Exception as e:
demisto.error(f'Failed checking for zombie processes: {e}. Trace: {traceback.format_exc()}')
def startServer(): # pragma: no cover
class fileHandler(http.server.BaseHTTPRequestHandler):
# See: https://docs.python.org/3/library/http.server.html#http.server.BaseHTTPRequestHandler.log_message
# Need to override otherwise messages are logged to stderr
def log_message(self, msg, *args):
demisto.debug("python http server log: " + (msg % args))
def do_GET(self):
demisto.debug(f'Handling MD Image request {self.path}')
img_path = MD_IMAGE_PATH_SAAS if is_demisto_version_ge(MD_IMAGE_SAAS_VERSION) else MD_IMAGE_PATH
if TENANT_ACCOUNT_NAME:
markdown_path_prefix = f"/{TENANT_ACCOUNT_NAME}{img_path}"
else:
markdown_path_prefix = img_path
if not self.path.startswith(markdown_path_prefix):
# not a standard xsoar markdown image endpoint
self.send_response(400)
self.flush_headers()
return
fileID = os.path.split(self.path)[1]
try:
res = demisto.getFilePath(fileID)
file_path = res.get('path')
if file_path == '':
demisto.debug(f'Failed to get markdown file {fileID}, empty filepath returned from xsoar')
self.send_response(404)
self.flush_headers()
return
name = res.get('name')
try:
# Open the file
with open(f'{file_path}', 'rb') as file:
self.send_response(200)
self.send_header("Content-type", "application/octet-stream")
self.send_header("Content-Disposition", f'attachment; filename={name}')
self.end_headers()
self.wfile.write(file.read()) # Read the file and send the contents
except BrokenPipeError: # ignore broken pipe as socket might have been closed
pass
except Exception as ex:
demisto.error(f'Failed to get markdown file: {fileID}, file path: {file_path}. Error: {ex}')
self.send_response(404)
self.flush_headers()
# Create server object listening the port 10888
global SERVER_OBJECT
SERVER_OBJECT = HTTPServer(server_address=('', MD_HTTP_PORT), RequestHandlerClass=fileHandler)
# Start the web server
demisto.debug(f"starting markdown server on port {MD_HTTP_PORT}")
SERVER_OBJECT.serve_forever()
def main(): # pragma: no cover
isMDImagesSupported = is_demisto_version_ge(MD_IMAGE_SUPPORT_MIN_VER)
try:
sane_json_b64 = demisto.args().get('sane_pdf_report_base64', '').encode(
'utf-8')
orientation = demisto.args().get('orientation', 'portrait')
resourceTimeout = demisto.args().get('resourceTimeout', '4000')
reportType = demisto.args().get('reportType', 'pdf')
headerLeftImage = demisto.args().get('customerLogo', '')
headerRightImage = demisto.args().get('demistoLogo', '')
pageSize = demisto.args().get('paperSize', 'letter')
disableHeaders = demisto.args().get('disableHeaders', '')
tableTextMaxLength = demisto.args().get('tableTextMaxLength', '300')
forceServerFormattedTimeString = demisto.args().get('forceServerFormattedTimeString', 'false')
addUtf8Bom = demisto.args().get('addUtf8Bom', 'false')
# Note: After headerRightImage the empty one is for legacy argv in server.js
extra_cmd = f"{orientation} {resourceTimeout} {reportType} " + \
f'"{headerLeftImage}" "{headerRightImage}" "" ' + \
f'"{pageSize}" "{disableHeaders}"'
isMDImagesSupported = is_demisto_version_ge(MD_IMAGE_SUPPORT_MIN_VER)
if isMDImagesSupported: # pragma: no cover
# start the server in a background thread
demisto.debug('Starting markdown artifacts http server...')
threading.Thread(target=startServer).start()
time.sleep(5)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex(('localhost', MD_HTTP_PORT))
if result == 0:
demisto.debug('Server is running')
sock.close()
else:
demisto.error('Markdown artifacts server is not responding')
# add md server address
mdServerAddress = f'http://localhost:{MD_HTTP_PORT}'
extra_cmd += f' "" "" "{mdServerAddress}"'
isTableTextMaxLengthSupported = is_demisto_version_ge(TABLE_TEXT_MAX_LENGTH_SUPPORT_MIN_VER)
if isTableTextMaxLengthSupported:
extra_cmd += f' {tableTextMaxLength}'
else:
extra_cmd += ' ""'
extra_cmd += f' "{forceServerFormattedTimeString}"'
extra_cmd += f' "{addUtf8Bom}"'
with tempfile.TemporaryDirectory(suffix='sane-pdf', ignore_cleanup_errors=True) as tmpdir: # type: ignore[call-overload]
input_file = tmpdir + '/input.json'
output_file = tmpdir + '/output.pdf'
dist_dir = tmpdir + '/dist'
shutil.copytree(WORKING_DIR / 'dist', dist_dir)
with open(input_file, 'wb') as f:
f.write(base64.b64decode(sane_json_b64))
if headerLeftImage:
customer_logo_file_path = tmpdir + "/customer-logo-base64.txt"
with open(customer_logo_file_path, "w") as f:
f.write(headerLeftImage)
extra_cmd = extra_cmd.replace(headerLeftImage, customer_logo_file_path)
headerLeftImage = customer_logo_file_path
cmd = ['./reportsServer', input_file, output_file, dist_dir] + shlex.split(
extra_cmd)
# Logging things for debugging
params = f'[orientation="{orientation}",' \
f' resourceTimeout="{resourceTimeout}",' \
f' reportType="{reportType}", headerLeftImage="{headerLeftImage}",' \
f' headerRightImage="{headerRightImage}", pageSize="{pageSize}",' \
f' disableHeaders="{disableHeaders}", forceServerFormattedTimeString="{forceServerFormattedTimeString}",' \
f' addUtf8Bom="{addUtf8Bom}"'
if isMDImagesSupported:
params += f', markdownArtifactsServerAddress="{mdServerAddress}"'
demisto.debug(f"Sane-PDF parameters: {params}]")
cmd_string = " ".join(cmd)
demisto.debug(f'Sane-PDF report commmad: {cmd_string}')
# Execute the report creation
out = subprocess.check_output(cmd, cwd=WORKING_DIR,
stderr=subprocess.STDOUT)
demisto.debug(f"Sane-pdf output: {str(out)}")
with open(output_file, 'rb') as f:
encoded = base64.b64encode(f.read()).decode('utf-8', 'ignore')
return_outputs(readable_output='Successfully generated pdf',
outputs={}, raw_response={'data': encoded})
except subprocess.CalledProcessError as e:
tb = traceback.format_exc()
wrap = "=====sane-pdf-reports error====="
err = f'{wrap}\n{tb}{wrap}, process error: {e.output}\n'
return_error(f'[SanePdfReports Automation Error - CalledProcessError] - {err}')
except Exception:
tb = traceback.format_exc()
wrap = "=====sane-pdf-reports error====="
err = f'{wrap}\n{tb}{wrap}\n'
return_error(f'[SanePdfReports Automation Error - Exception] - {err}')
finally:
quit_driver_and_reap_children(isMDImagesSupported)
if __name__ in ['__main__', '__builtin__', 'builtins']:
main()