/
rest_api.py
393 lines (316 loc) · 14.2 KB
/
rest_api.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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
"""Declaration of the custom REST API to graph DB."""
import os
import flask
from flask import Flask, request
from flask_cors import CORS
import json
from src import data_importer
from src.graph_manager import BayesianGraph
from src.graph_populator import GraphPopulator
from src.cve import CVEPut, CVEDelete, CVEGet, CVEDBVersion, SnykCVEPut, SnykCVEDelete
from raven.contrib.flask import Sentry
from src import config as config
from werkzeug.contrib.fixers import ProxyFix
from werkzeug.exceptions import InternalServerError
import logging
from flask import Blueprint, current_app
from src.utils import rectify_latest_version, sync_all_non_cve_version,\
sync_all_latest_version
api_v1 = Blueprint('api_v1', __name__)
@api_v1.route('/api/v1/readiness')
def readiness():
"""Generate response for the GET request to /api/v1/readiness."""
return flask.jsonify({}), 200
@api_v1.route('/api/v1/liveness')
def liveness():
"""Generate response for the GET request to /api/v1/liveness."""
# TODO Check graph database connection
return flask.jsonify({}), 200
@api_v1.route('/api/v1/sync_latest_version', methods=['POST'])
def sync_latest_version():
"""Post request to sync all the EPV's latest version to Graph."""
input_json = request.get_json()
current_app.logger.info("List of EPVs for version rectification- " + json.dumps(input_json))
if len(input_json) == 1 and input_json[0] == "all":
report = sync_all_latest_version("src/data_source/all_packages.json")
else:
report = rectify_latest_version(input_json)
response = {'message': report.get('message')}
if report.get('status') != 'Success':
return flask.jsonify(response), 500
else:
return flask.jsonify(response)
@api_v1.route('/api/v1/sync_latest_non_cve_version', methods=['POST'])
def sync_latest_non_cve_version():
"""Post request to sync all the pkgs with latest non cve version to Graph."""
input_json = request.get_json()
current_app.logger.info("Latest non cve version to be synced for- " + json.dumps(input_json))
# Update all the pkg nodes with latest non cve version for eco provided
report = sync_all_non_cve_version(input_json)
response = {'message': report.get('message')}
if report.get('status') != 'Success':
return flask.jsonify(response), 500
else:
return flask.jsonify(response)
@api_v1.route('/api/v1/pending')
def pending():
"""Get request to enlist all the EPVs which are not yet synced to Graph."""
current_app.logger.info("/api/v1/pending - %s" % dict(request.args))
ecosystem_name = request.args.get('ecosystem', None)
package_name = request.args.get('package', None)
version_id = request.args.get('version', None)
limit = request.args.get('limit', None)
offset = request.args.get('offset', None)
params = {"ecosystem": ecosystem_name, "package": package_name, "version": version_id,
"limit": limit, "offset": offset}
current_app.logger.info("params - %s" % params)
pending_list = data_importer.PostgresHandler().fetch_pending_epvs(**params)
return flask.jsonify(pending_list), 200
@api_v1.route('/api/v1/sync_all')
def sync_all():
"""Generate response for the GET request to /api/v1/sync_all."""
current_app.logger.info("/api/v1/sync_all - %s" % dict(request.args))
ecosystem_name = request.args.get('ecosystem', None)
package_name = request.args.get('package', None)
version_id = request.args.get('version', None)
limit = request.args.get('limit', None)
offset = request.args.get('offset', None)
params = {"ecosystem": ecosystem_name, "package": package_name, "version": version_id,
"limit": limit, "offset": offset}
current_app.logger.info("params - %s" % params)
data = data_importer.PostgresHandler().fetch_pending_epvs(**params)
try:
pending_list = data["pending_list"]
report = data_importer.import_epv_from_s3_http(list_epv=pending_list)
response = {'message': report.get('message'),
'epv': pending_list,
'count_imported_EPVs': report.get('count_imported_EPVs')}
if report.get('status') != 'Success':
return flask.jsonify(response), 500
else:
return flask.jsonify(response)
except RuntimeError:
response = {'message': 'RuntimeError encountered', 'epv': pending_list}
return flask.jsonify(response), 500
@api_v1.route('/api/v1/ingest_to_graph', methods=['POST'])
def ingest_to_graph():
"""Import e/p/v data and generate response for the POST request to /api/v1/ingest_to_graph."""
input_json = request.get_json()
current_app.logger.info("Ingesting the given list of EPVs - " + json.dumps(input_json))
expected_keys = set(['ecosystem', 'name', 'version'])
for epv in input_json:
if not expected_keys.issubset(set(epv.keys())):
response = {'message': 'Invalid keys found in input: ' + ','.join(epv.keys())}
return flask.jsonify(response), 400
report = data_importer.import_epv_from_s3_http(list_epv=input_json)
response = {'message': report.get('message'),
'epv': input_json,
'count_imported_EPVs': report.get('count_imported_EPVs')}
print(response)
# TODO the previous code can raise a runtime exception, does not we need to handle that?
if report.get('status') != 'Success':
return flask.jsonify(response), 500
else:
return flask.jsonify(response)
@api_v1.route('/api/v1/create_nodes', methods=['POST'])
def create_nodes():
"""Create e/p/v graph node and generate response for the POST request to/api/v1/create_nodes."""
input_json = request.get_json()
if not input_json:
return flask.jsonify(message="No EPVs provided. Please provide valid list of EPVs"), 400
expected_keys = set(['ecosystem', 'name', 'version'])
for epv in input_json:
# We expect alteast Ecosystem, Package and Version as a payload
if not expected_keys.issubset(set(epv.keys())):
response = {'message': 'Invalid keys found in input: ' + ','.join(epv.keys())}
return flask.jsonify(response), 400
current_app.logger.info("Creating the nodes for EPVs - " + json.dumps(input_json))
report = data_importer.create_graph_nodes(list_epv=input_json)
current_app.logger.info(report)
if report.get('status') != 'Success':
return flask.jsonify(report), 500
else:
return flask.jsonify(report)
@api_v1.route('/api/v1/selective_ingest', methods=['POST'])
def selective_ingest():
"""Import e/p/v data and generate response for the POST request to /api/v1/selective_ingest."""
input_json = request.get_json()
if input_json.get('package_list') is None or len(input_json.get('package_list')) == 0:
return flask.jsonify(message='No Packages provided. Nothing to be ingested'), 400
expected_keys = set(['ecosystem', 'name'])
for epv in input_json.get('package_list'):
if not expected_keys.issubset(set(epv.keys())):
response = {'message': 'Invalid keys found in input: ' + ','.join(epv.keys())}
return flask.jsonify(response), 400
current_app.logger.info("Selective Ingestion with payload - " + json.dumps(input_json))
report = data_importer.import_epv_from_s3_http(list_epv=input_json.get('package_list'),
select_doc=input_json.get('select_ingest', None))
response = {'message': report.get('message'),
'epv': input_json,
'count_imported_EPVs': report.get('count_imported_EPVs')}
current_app.logger.info(response)
# TODO the previous code can raise a runtime exception, does not we need to handle that?
if report.get('status') != 'Success':
return flask.jsonify(response), 500
else:
return flask.jsonify(response)
@api_v1.route('/api/v1/vertex/<string:ecosystem>/<string:package>/<string:version>/properties',
methods=['PUT', 'DELETE'])
def handle_properties(ecosystem, package, version):
"""
Handle (update/delete) properties associated with given EPV.
Update replaces properties with the same name.
Expects JSON payload in following format:
{
"properties": [
{
"name": "cve_ids",
"value": "CVE-3005-0001:10"
}
]
}
"value" can be omitted in DELETE requests.
:param ecosystem: str, ecosystem
:param package: str, package name
:param version: str, package version
:return: 200 on success, 400 on failure
"""
# TODO: reduce cyclomatic complexity
input_json = request.get_json()
properties = input_json.get('properties')
error = flask.jsonify({'error': 'invalid input'})
if not properties:
return error, 400
input_json = {k: GraphPopulator.sanitize_text_for_query(str(v)) for k, v in input_json.items()}
if request.method == 'PUT':
if [x for x in properties if not x.get('name') or x.get('value') is None]:
return error, 400
log_msg = '[{m}] Updating properties for {e}/{p}/{v} with payload {b}'
current_app.logger.info(log_msg.format(m=request.method, e=ecosystem, p=package,
v=version, b=input_json))
query_statement = "g.V()" \
".has('pecosystem','{ecosystem}')" \
".has('pname','{pkg_name}')" \
".has('version','{version}')".format(ecosystem=ecosystem,
pkg_name=package,
version=version)
statement = ''
if request.method in ('DELETE', 'PUT'):
# build "delete" part of the statement
drop_str = ""
for prop in properties:
drop_str += query_statement
drop_str += ".properties('{property}').drop().iterate();".format(property=prop['name'])
statement += drop_str
if request.method == 'PUT':
# build "add" part of the statement
add_str = ""
for prop in properties:
add_str += ".property('{property}','{value}')".format(
property=prop['name'], value=prop['value']
)
statement += query_statement + add_str + ';'
current_app.logger.info('Gremlin statement: {s}'.format(s=statement))
success, response_json = BayesianGraph.execute(statement)
if not success:
current_app.logger.error("Failed to update properties for {e}/{p}/{v}".format(
e=ecosystem, p=package, v=version)
)
return flask.jsonify(response_json), 400
return flask.jsonify(response_json), 200
@api_v1.route('/api/v1/cves', methods=['PUT', 'DELETE'])
def cves_put_delete():
"""Put or delete CVE nodes.
Missing EPVs will be created.
"""
payload = request.get_json(silent=True)
try:
if request.method == 'PUT':
cve = CVEPut(payload)
elif request.method == 'DELETE':
cve = CVEDelete(payload)
else:
# this should never happen
return flask.jsonify({'error': 'method not allowed'}), 405
except ValueError as e:
return flask.jsonify({'error': str(e)}), 400
try:
cve.process()
except ValueError as e:
return flask.jsonify({'error': str(e)}), 500
return flask.jsonify({}), 200
@api_v1.route('/api/v1/snyk-cves', methods=['PUT', 'DELETE'])
def snyk_cves_put_delete():
"""Put or delete Snyk CVE nodes.
Missing EPVs will be created.
"""
payload = request.get_json(silent=True)
try:
if request.method == 'PUT':
cve = SnykCVEPut(payload)
elif request.method == 'DELETE':
cve = SnykCVEDelete(payload)
else:
# this should never happen
return flask.jsonify({'error': 'method not allowed'}), 405
except ValueError as e:
return flask.jsonify({'error': str(e)}), 400
try:
cve.process()
except InternalServerError as e:
return flask.jsonify({'error': str(e)}), 500
return flask.jsonify({}), 200
@api_v1.route('/api/v1/cves/<string:ecosystem>', methods=['GET'])
@api_v1.route('/api/v1/cves/<string:ecosystem>/<string:name>', methods=['GET'])
@api_v1.route('/api/v1/cves/<string:ecosystem>/<string:name>/<string:version>', methods=['GET'])
def cves_get(ecosystem, name=None, version=None):
"""Get list of CVEs for E, EP, or EPV."""
cve = CVEGet(ecosystem, name, version)
try:
result = cve.get()
except ValueError as e:
return flask.jsonify({'error': str(e)}), 500
return flask.jsonify(result), 200
@api_v1.route('/api/v1/cvedb-version', methods=['GET'])
def cvedb_version_get():
"""Get CVEDB version."""
try:
version = CVEDBVersion().get()
except ValueError as e:
return flask.jsonify({'error': str(e)}), 500
return flask.jsonify({'version': version}), 200
@api_v1.route('/api/v1/cvedb-version', methods=['PUT'])
def cvedb_version_put():
"""Create or replace CVEDB version."""
payload = request.get_json(silent=True)
if not payload or 'version' not in payload:
return flask.jsonify({'error': 'invalid input'}), 400
try:
version = CVEDBVersion().put(payload)
except ValueError as e:
return flask.jsonify({'error': str(e)}), 500
return flask.jsonify({'version': version}), 200
def create_app():
"""Create Flask app object."""
new_app = Flask(config.APP_NAME)
new_app.config.from_object('src.config')
CORS(new_app)
new_app.register_blueprint(api_v1)
return new_app
def setup_logging(flask_app):
"""Perform the setup of logging for this application."""
if not flask_app.debug:
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s'))
log_level = os.environ.get('FLASK_LOGGING_LEVEL', logging.getLevelName(logging.WARNING))
handler.setLevel(log_level)
flask_app.logger.addHandler(handler)
flask_app.config['LOGGER_HANDLER_POLICY'] = 'never'
flask_app.logger.setLevel(logging.DEBUG)
app = create_app()
setup_logging(app)
app.wsgi_app = ProxyFix(app.wsgi_app)
sentry = Sentry(app, dsn=config.SENTRY_DSN, logging=True, level=logging.ERROR)
if __name__ == "__main__":
app.run()