Skip to content

Commit 91bf58b

Browse files
committed
Adding --api-test for CI/CD
1 parent d570f8e commit 91bf58b

8 files changed

Lines changed: 292 additions & 10 deletions

File tree

.github/workflows/tests.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ jobs:
4747
run: python -B -m unittest discover -s tests -p "test_*.py"
4848

4949
- name: Smoke test
50-
run: python sqlmap.py --smoke
50+
run: python sqlmap.py --smoke-test
5151

5252
- name: Vuln test
53-
run: python sqlmap.py --vuln
53+
run: python sqlmap.py --vuln-test
54+
55+
- name: API test
56+
run: python sqlmap.py --api-test

data/txt/sha256sums.txt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,26 +180,26 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.
180180
5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py
181181
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py
182182
914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py
183-
885042ed021e60f1739e2a849e3405cc3a4c2a67a5a169a30399d1c53446460f lib/core/optiondict.py
183+
3ec59b5eb336d9808d28496f1cbbad716b4a0e276b5399023142826e460e3fd2 lib/core/optiondict.py
184184
3ff871fe8391952c3ec3bb528ba592a13926c80ca0b68fd322a317f69a651ef7 lib/core/option.py
185185
ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch.py
186186
49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py
187187
03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py
188188
48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py
189189
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
190190
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
191-
70ddf88d4efda5486853c3616ba64114757a836640585ddae309dd3d335d697a lib/core/settings.py
191+
d9180ce5490c781b8f8771b0d5754d27f550aae963ad36731e0d0941a0f8590c lib/core/settings.py
192192
cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py
193193
bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py
194194
70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py
195-
8bbc9312147ee8ca719860bc7ad472eac25230e4d46976fbb405efe43fe15ef6 lib/core/testing.py
195+
daf2ad65fcea430b6272e3c538022c9871fdc3aba78f71669130fb0bc954c78e lib/core/testing.py
196196
e3e653364d08d04d7492aa40a2bd29c6a28f4d78fecdd6c10f21f6cb28b98b4c lib/core/threads.py
197197
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
198198
53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py
199199
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
200200
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
201201
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
202-
3f298a58a41225ef67c57b2cf08c71f2eacbab8f98463b4461f45933d6a82f69 lib/parse/cmdline.py
202+
053079fe796dfce09cf94ac6f094043f2dfa393b5631387fadb4f735cf1ac6a4 lib/parse/cmdline.py
203203
02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py
204204
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
205205
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
@@ -492,7 +492,7 @@ cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generi
492492
46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py
493493
f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml
494494
627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf
495-
d5128ba488b85080a18df85cc08b58f0baeac59494eb5ef43b9e34d66538f091 sqlmap.py
495+
f8974aac701639b54ca34b0e11803c836e5cb1e1c5a6eaf275315949b6487310 sqlmap.py
496496
eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py
497497
a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py
498498
cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py
@@ -585,6 +585,7 @@ c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_has
585585
205e84827461101a78b2cffaa3de49795a1214e92276fc7fd40f3456657062b9 tests/test_identifiers_output.py
586586
5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py
587587
caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py
588+
57fa9713a3186020be8bcc3f06399e92bf9ce82ec6d3413c76babe19606bb698 tests/test_openapi_drift.py
588589
cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py
589590
4bac34af2abddce003756d6776e89b2fda220bb7603ef3761f4f37ee29f9c369 tests/test_payload_marking.py
590591
6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py

lib/core/optiondict.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@
273273
"forceDns": "boolean",
274274
"murphyRate": "integer",
275275
"smokeTest": "boolean",
276+
"apiTest": "boolean",
276277
},
277278

278279
"API": {

lib/core/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.6.110"
23+
VERSION = "1.10.6.111"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

lib/core/testing.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
"""
77

88
import doctest
9+
import json
910
import logging
1011
import os
1112
import random
1213
import re
1314
import socket
1415
import sqlite3
16+
import subprocess
1517
import sys
1618
import tempfile
1719
import threading
@@ -20,17 +22,22 @@
2022
from extra.vulnserver import vulnserver
2123
from lib.core.common import clearConsoleLine
2224
from lib.core.common import dataToStdout
25+
from lib.core.common import getSafeExString
2326
from lib.core.common import randomInt
2427
from lib.core.common import randomStr
2528
from lib.core.common import shellExec
2629
from lib.core.compat import round
30+
from lib.core.compat import xrange
2731
from lib.core.convert import encodeBase64
32+
from lib.core.convert import getBytes
33+
from lib.core.convert import getText
2834
from lib.core.data import kb
2935
from lib.core.data import logger
3036
from lib.core.data import paths
3137
from lib.core.data import queries
3238
from lib.core.patch import unisonRandom
3339
from lib.core.settings import IS_WIN
40+
from lib.core.settings import RESTAPI_VERSION
3441

3542
def vulnTest():
3643
"""
@@ -224,6 +231,156 @@ def _thread():
224231

225232
return retVal
226233

234+
def apiTest():
235+
"""
236+
Runs a basic live test of the REST API: launches the server in a separate process
237+
('sqlmapapi.py -s') and drives the control-plane endpoints with an HTTP client - a real
238+
server + client round-trip, without launching an actual scan. A separate process (rather
239+
than an in-process thread) isolates the single-threaded server from the client's GIL and
240+
from sqlmap's global HTTP machinery, which otherwise makes the round-trip flaky.
241+
"""
242+
243+
retVal = True
244+
245+
# pick a free port the same way vulnTest() does
246+
while True:
247+
address, port = "127.0.0.1", random.randint(10000, 65535)
248+
try:
249+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
250+
if s.connect_ex((address, port)):
251+
break
252+
else:
253+
time.sleep(1)
254+
finally:
255+
s.close()
256+
257+
username, password = "test", "test"
258+
apipath = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "sqlmapapi.py"))
259+
260+
try:
261+
devnull = subprocess.DEVNULL
262+
except AttributeError:
263+
devnull = open(os.devnull, "wb")
264+
265+
process = subprocess.Popen([sys.executable, apipath, "-s", "-H", address, "-p", str(port), "--username", username, "--password", password], stdout=devnull, stderr=devnull)
266+
267+
base = "http://%s:%d" % (address, port)
268+
269+
def _call(path, data=None, authorize=True):
270+
# NOTE: a raw socket is used deliberately instead of urllib/http.client. The host sqlmap
271+
# process installs a global keep-alive opener and patches http.client, which makes a
272+
# library client flaky against the single-threaded server; a hand-rolled HTTP/1.0 request
273+
# (Connection: close, read to EOF) is hermetic and immune to all of that.
274+
method = "POST" if data is not None else "GET"
275+
lines = ["%s %s HTTP/1.0" % (method, path), "Host: %s:%d" % (address, port)]
276+
if authorize:
277+
lines.append("Authorization: Basic %s" % encodeBase64("%s:%s" % (username, password), binary=False))
278+
body = getBytes(json.dumps(data)) if data is not None else b""
279+
if data is not None:
280+
lines.append("Content-Type: application/json")
281+
lines.append("Content-Length: %d" % len(body))
282+
lines.append("Connection: close")
283+
request = getBytes("\r\n".join(lines) + "\r\n\r\n") + body
284+
285+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
286+
s.settimeout(10)
287+
try:
288+
s.connect((address, port))
289+
s.sendall(request)
290+
raw = b""
291+
while True:
292+
chunk = s.recv(8192)
293+
if not chunk:
294+
break
295+
raw += chunk
296+
except Exception as ex:
297+
logger.debug("API test: request to '%s' failed (%s)" % (path, getSafeExString(ex)))
298+
return None, None
299+
finally:
300+
s.close()
301+
302+
head, _, payload = raw.partition(b"\r\n\r\n")
303+
try:
304+
code = int(head.split(b"\r\n")[0].split(b" ")[1])
305+
except (IndexError, ValueError):
306+
return None, None
307+
try:
308+
return code, json.loads(getText(payload))
309+
except ValueError:
310+
return code, None
311+
312+
try:
313+
# wait for the server process to come up (or die trying)
314+
for _ in xrange(200):
315+
if process.poll() is not None:
316+
logger.error("API test: server process exited prematurely (address: '%s')" % base)
317+
return False
318+
code, data = _call("/version")
319+
if code == 200 and data and data.get("success"):
320+
break
321+
time.sleep(0.1)
322+
else:
323+
logger.error("API test: server did not come up (address: '%s')" % base)
324+
return False
325+
326+
logger.info("REST API server running at '%s'..." % base)
327+
328+
results = []
329+
330+
def _check(name, condition):
331+
results.append((name, bool(condition)))
332+
if not condition:
333+
logger.error("API test: check '%s' FAILED" % name)
334+
335+
# GET /version - success envelope + MAJOR-only integer api_version
336+
code, data = _call("/version")
337+
_check("version", code == 200 and data and data.get("success") is True and data.get("api_version") == int(RESTAPI_VERSION.split(".")[0]) and data.get("version"))
338+
339+
# the auth hook must reject an unauthenticated request
340+
code, _ = _call("/version", authorize=False)
341+
_check("auth-401", code == 401)
342+
343+
# GET /task/new - mint a task
344+
code, data = _call("/task/new")
345+
taskid = data.get("taskid") if data else None
346+
_check("task-new", code == 200 and data and data.get("success") and taskid)
347+
348+
# POST /option/<taskid>/set then read it back via /get and /list (JSON round-trip + IPC)
349+
code, data = _call("/option/%s/set" % taskid, {"flushSession": True})
350+
_check("option-set", code == 200 and data and data.get("success"))
351+
352+
code, data = _call("/option/%s/get" % taskid, ["flushSession"])
353+
_check("option-get", data and data.get("success") and (data.get("options") or {}).get("flushSession") is True)
354+
355+
code, data = _call("/option/%s/list" % taskid)
356+
_check("option-list", data and data.get("success") and isinstance(data.get("options"), dict))
357+
358+
# GET /admin/list - the IP-bound listing (our client is the task's creator) must see it
359+
code, data = _call("/admin/list")
360+
_check("admin-list", data and data.get("success") and taskid in (data.get("tasks") or {}))
361+
362+
# a bogus task ID must produce a failure envelope (not a crash)
363+
code, data = _call("/option/%s/list" % "nonexistent")
364+
_check("invalid-task", data is not None and data.get("success") is False)
365+
366+
# GET /task/<taskid>/delete - tear the task down
367+
code, data = _call("/task/%s/delete" % taskid)
368+
_check("task-delete", data and data.get("success"))
369+
370+
if all(ok for _, ok in results):
371+
logger.info("API test final result: PASSED")
372+
else:
373+
retVal = False
374+
logger.error("API test final result: FAILED (%s)" % ", ".join(name for name, ok in results if not ok))
375+
finally:
376+
try:
377+
process.terminate()
378+
process.wait()
379+
except Exception:
380+
pass
381+
382+
return retVal
383+
227384
def smokeTest():
228385
"""
229386
Runs the basic smoke testing of a program

lib/parse/cmdline.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,9 @@ def cmdLineParser(argv=None):
875875
parser.add_argument("--vuln-test", dest="vulnTest", action="store_true",
876876
help=SUPPRESS)
877877

878+
parser.add_argument("--api-test", dest="apiTest", action="store_true",
879+
help=SUPPRESS)
880+
878881
parser.add_argument("--disable-json", dest="disableJson", action="store_true",
879882
help=SUPPRESS)
880883

@@ -1129,7 +1132,7 @@ def _format_action_invocation(self, action):
11291132
else:
11301133
args.stdinPipe = None
11311134

1132-
if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)):
1135+
if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)):
11331136
errMsg = "missing a mandatory option (-d, -u, -l, -m, -r, -g, -c, --wizard, --shell, --update, --purge, --list-tampers or --dependencies). "
11341137
errMsg += "Use -h for basic and -hh for advanced help\n"
11351138
parser.error(errMsg)

sqlmap.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ def main():
188188
elif conf.vulnTest:
189189
from lib.core.testing import vulnTest
190190
os._exitcode = 1 - (vulnTest() or 0)
191+
elif conf.apiTest:
192+
from lib.core.testing import apiTest
193+
os._exitcode = 1 - (apiTest() or 0)
191194
else:
192195
from lib.controller.controller import start
193196
if conf.profile:
@@ -600,7 +603,7 @@ def main():
600603
except OSError:
601604
pass
602605

603-
if any((conf.vulnTest, conf.smokeTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files
606+
if any((conf.vulnTest, conf.smokeTest, conf.apiTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files
604607
try:
605608
shutil.rmtree(tempDir, ignore_errors=True)
606609
except OSError:

0 commit comments

Comments
 (0)