Skip to content

Commit 403855f

Browse files
committed
Adding JSONL as a dump format
1 parent 17e94c3 commit 403855f

6 files changed

Lines changed: 185 additions & 9 deletions

File tree

data/txt/sha256sums.txt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.
175175
70fb2528e580b22564899595b0dff6b1bc257c6a99d2022ce3996a3d04e68e4e lib/core/decorators.py
176176
147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py
177177
2f44a1bfe6f18aafe64147b99e69aa93cf438c0e7befe59f4e2aee9065c8b7b6 lib/core/dicts.py
178-
e4b23512625bc377c0e0924d8113c595452320d8c66014828da5d8258a77f55a lib/core/dump.py
179-
23e33f0b457e2a7114c9171ba9b42e1751b71ee3f384bba7fad39e4490adb803 lib/core/enums.py
178+
2592b0fd38c272c0b0d49878f4449437eb8ba8ff7536bb39b2ac9a2511010f7c lib/core/dump.py
179+
6b9932d9c789a0e2ac28a493fb7914f49100a1c91de989bcdb20df9d40648522 lib/core/enums.py
180180
5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py
181181
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py
182182
914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py
@@ -188,7 +188,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch
188188
48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py
189189
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
190190
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
191-
1e2a5277293de9d3d1e65b401013baf1c4033162e580f6891ca6a2686e666894 lib/core/settings.py
191+
72448bcfc929496fb0333480a780163a395f65fff92898ad8108daf54a12799b lib/core/settings.py
192192
cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py
193193
bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py
194194
70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py
@@ -199,7 +199,7 @@ b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unesc
199199
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
200200
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
201201
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
202-
7bc8612fbd7ba390ab19f908c370c126ae66afa200bc7975800599ecbe029f0c lib/parse/cmdline.py
202+
3f298a58a41225ef67c57b2cf08c71f2eacbab8f98463b4461f45933d6a82f69 lib/parse/cmdline.py
203203
02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py
204204
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
205205
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
@@ -577,6 +577,7 @@ a48c411fea864e6bcd6a1c7e1a35094b8cda8d15088fd9e7b0270542ae20daa9 tests/test_com
577577
3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py
578578
e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py
579579
993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py
580+
c706c5dad287e2e8cf707f7aa5eeb9394eddc6ef3a4fea809babf3ae77e8d7fa tests/test_dump_jsonl.py
580581
2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py
581582
bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py
582583
8105de9978fe286a29f6b635a58db1e9998d86e8dded54d7efdfb9d52a121094 tests/test_hashdb.py

lib/core/dump.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77

88
import hashlib
9+
import json
910
import os
1011
import re
1112
import shutil
@@ -61,6 +62,7 @@
6162
from lib.utils.safe2bin import safechardecode
6263
from thirdparty import six
6364
from thirdparty.magic import magic
65+
from thirdparty.odict import OrderedDict
6466

6567
class Dump(object):
6668
"""
@@ -461,7 +463,7 @@ def dbTableValues(self, tableValues):
461463

462464
if conf.dumpFormat == DUMP_FORMAT.SQLITE:
463465
replication = Replication(os.path.join(conf.dumpPath, "%s.sqlite3" % safeDb))
464-
elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML):
466+
elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML, DUMP_FORMAT.JSONL):
465467
if not os.path.isdir(dumpDbPath):
466468
try:
467469
os.makedirs(dumpDbPath)
@@ -624,6 +626,7 @@ def dbTableValues(self, tableValues):
624626
console = (i >= count - TRIM_STDOUT_DUMP_SIZE)
625627
field = 1
626628
values = []
629+
record = OrderedDict()
627630

628631
if i == 0 and count > TRIM_STDOUT_DUMP_SIZE:
629632
self._write(" ...")
@@ -674,6 +677,11 @@ def dbTableValues(self, tableValues):
674677
dataToDumpFile(dumpFP, "%s%s" % (safeCSValue(value), conf.csvDel))
675678
elif conf.dumpFormat == DUMP_FORMAT.HTML:
676679
dataToDumpFile(dumpFP, "<td>%s</td>" % getUnicode(htmlEscape(value).encode("ascii", "xmlcharrefreplace")))
680+
elif conf.dumpFormat == DUMP_FORMAT.JSONL:
681+
if len(info["values"]) <= i or info["values"][i] is None or info["values"][i] == " ": # NULL
682+
record[unsafeSQLIdentificatorNaming(column)] = None
683+
else:
684+
record[unsafeSQLIdentificatorNaming(column)] = getUnicode(info["values"][i])
677685

678686
field += 1
679687

@@ -686,6 +694,8 @@ def dbTableValues(self, tableValues):
686694
dataToDumpFile(dumpFP, "\n")
687695
elif conf.dumpFormat == DUMP_FORMAT.HTML:
688696
dataToDumpFile(dumpFP, "</tr>\n")
697+
elif conf.dumpFormat == DUMP_FORMAT.JSONL:
698+
dataToDumpFile(dumpFP, "%s\n" % getUnicode(json.dumps(record, ensure_ascii=False)))
689699

690700
self._write("|", console=console)
691701

@@ -695,10 +705,10 @@ def dbTableValues(self, tableValues):
695705
rtable.endTransaction()
696706
logger.info("table '%s.%s' dumped to SQLITE database '%s'" % (db, table, replication.dbpath))
697707

698-
elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML):
708+
elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML, DUMP_FORMAT.JSONL):
699709
if conf.dumpFormat == DUMP_FORMAT.HTML:
700710
dataToDumpFile(dumpFP, "</tbody>\n</table>\n<script>let lc=-1,ld=1;function sortTable(n,h){var t=document.querySelector(\"table\"),r=Array.from(t.tBodies[0].rows);ld=(lc==n?-ld:1);lc=n;r.sort((a,b)=>{var x=a.cells[n].innerText.trim(),y=b.cells[n].innerText.trim(),nx=parseFloat(x),ny=parseFloat(y);return(!isNaN(nx)&&!isNaN(ny)?(nx-ny)*ld:x.localeCompare(y)*ld)});r.forEach(e=>t.tBodies[0].appendChild(e));Array.from(t.tHead.rows[0].cells).forEach(c=>{c.innerText=c.innerText.replace(/[\u2191\u2193]/g,\"\")});h.innerText=h.innerText+ (ld==1?\"\u2191\":\"\u2193\");}</script>\n</body>\n</html>")
701-
else:
711+
elif conf.dumpFormat == DUMP_FORMAT.CSV:
702712
dataToDumpFile(dumpFP, "\n")
703713
dumpFP.close()
704714

lib/core/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ class DUMP_FORMAT(object):
238238
CSV = "CSV"
239239
HTML = "HTML"
240240
SQLITE = "SQLITE"
241+
JSONL = "JSONL"
241242

242243
class HTTP_HEADER(object):
243244
ACCEPT = "Accept"

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.108"
23+
VERSION = "1.10.6.109"
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/parse/cmdline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ def cmdLineParser(argv=None):
686686
help="Store dumped data to a custom file")
687687

688688
general.add_argument("--dump-format", dest="dumpFormat",
689-
help="Format of dumped data (CSV (default), HTML or SQLITE)")
689+
help="Dump data format (CSV (default), HTML, SQLITE, JSONL)")
690690

691691
general.add_argument("--encoding", dest="encoding",
692692
help="Character encoding used for data retrieval (e.g. GBK)")

tests/test_dump_jsonl.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
5+
See the file 'LICENSE' for copying permission
6+
7+
JSONL output of the per-table dumper (Dump.dbTableValues in lib/core/dump.py).
8+
9+
--dump-format=JSONL writes one self-describing JSON object per row to a
10+
<host>/dump/<db>/<table>.jsonl file, streaming-safe (one independent line per
11+
row, no surrounding array/header/footer). These tests pin the contract that an
12+
automated consumer relies on: column order preserved (so it matches the CSV
13+
column order and is reproducible on Python 2's unordered dict), the DB-NULL
14+
marker (" ") mapped to JSON null exactly like --report-json, the empty string
15+
left intact (NOT collapsed to null), and a strict one-object-per-line layout.
16+
"""
17+
18+
import json
19+
import os
20+
import shutil
21+
import sys
22+
import tempfile
23+
import unittest
24+
25+
from collections import OrderedDict
26+
27+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
28+
from _testutils import bootstrap
29+
bootstrap()
30+
31+
from lib.core.common import Backend
32+
from lib.core.data import conf, kb
33+
from lib.core.dump import Dump
34+
from lib.core.enums import DUMP_FORMAT
35+
36+
37+
class _JsonlDumpCase(unittest.TestCase):
38+
def setUp(self):
39+
self._saved = dict((k, conf.get(k)) for k in ("dumpFormat", "dumpPath", "dumpFile", "col", "api", "reportCollector", "limitStart", "limitStop", "csvDel", "forceDbms", "dbms"))
40+
self._savedKb = dict((k, kb.get(k)) for k in ("forcedDbms", "dbms"))
41+
# A DBMS leaked from an earlier test (e.g. one that uppercases identifiers) would change
42+
# both the on-disk filename and the JSON keys, so pin a neutral, case-preserving back-end.
43+
conf.forceDbms = conf.dbms = None
44+
kb.dbms = None
45+
Backend.forceDbms("MySQL")
46+
self.tmp = tempfile.mkdtemp(prefix="sqlmap-jsonl-test")
47+
conf.dumpFormat = DUMP_FORMAT.JSONL
48+
conf.dumpPath = self.tmp
49+
conf.dumpFile = None
50+
conf.col = None
51+
conf.api = False
52+
conf.reportCollector = None
53+
conf.limitStart = conf.limitStop = None
54+
conf.csvDel = ","
55+
self.d = Dump()
56+
self.d._write = lambda *a, **k: None # silence the console table
57+
58+
def tearDown(self):
59+
for k, v in self._saved.items():
60+
conf[k] = v
61+
for k, v in self._savedKb.items():
62+
kb[k] = v
63+
shutil.rmtree(self.tmp, ignore_errors=True)
64+
65+
def _dump(self, table_values):
66+
self.d.dbTableValues(table_values)
67+
db = table_values["__infos__"]["db"] or "All"
68+
path = os.path.join(self.tmp, db, "%s.jsonl" % table_values["__infos__"]["table"])
69+
with open(path) as f:
70+
content = f.read()
71+
return content
72+
73+
def _rows(self, content):
74+
return [json.loads(line) for line in content.splitlines() if line.strip()]
75+
76+
77+
class TestJsonlContract(_JsonlDumpCase):
78+
def test_one_object_per_row(self):
79+
content = self._dump({
80+
"__infos__": {"count": 2, "db": "testdb", "table": "users"},
81+
"id": {"length": 2, "values": ["1", "2"]},
82+
"name": {"length": 6, "values": ["luther", "fluffy"]},
83+
})
84+
# exactly N non-empty lines, each terminated by a newline, each a standalone object
85+
lines = content.splitlines()
86+
self.assertEqual(len(lines), 2)
87+
self.assertTrue(content.endswith("\n"))
88+
rows = self._rows(content)
89+
self.assertEqual(rows[0], {"id": "1", "name": "luther"})
90+
self.assertEqual(rows[1], {"id": "2", "name": "fluffy"})
91+
92+
def test_no_header_or_footer(self):
93+
# unlike CSV (header row) / HTML (doc scaffold), JSONL must be pure data lines
94+
content = self._dump({
95+
"__infos__": {"count": 1, "db": "testdb", "table": "t"},
96+
"id": {"length": 2, "values": ["1"]},
97+
})
98+
lines = [l for l in content.splitlines() if l.strip()]
99+
self.assertEqual(len(lines), 1)
100+
self.assertEqual(json.loads(lines[0]), {"id": "1"})
101+
102+
def test_db_null_becomes_json_null(self):
103+
# sqlmap stores a DB NULL as a single space (" "); the machine format must emit JSON null,
104+
# consistent with --report-json. An empty string is a real value and must stay "".
105+
content = self._dump({
106+
"__infos__": {"count": 1, "db": "testdb", "table": "t"},
107+
"a": {"length": 1, "values": [" "]}, # DB NULL marker
108+
"b": {"length": 1, "values": [""]}, # genuine empty string
109+
"c": {"length": 1, "values": ["x"]},
110+
})
111+
row = self._rows(content)[0]
112+
self.assertIsNone(row["a"])
113+
self.assertEqual(row["b"], "")
114+
self.assertEqual(row["c"], "x")
115+
116+
def test_missing_value_is_null(self):
117+
# a column whose values list is short for this row index must serialize as null, not crash
118+
content = self._dump({
119+
"__infos__": {"count": 2, "db": "testdb", "table": "t"},
120+
"id": {"length": 2, "values": ["1", "2"]},
121+
"lagging": {"length": 4, "values": ["only-one"]}, # missing index 1
122+
})
123+
rows = self._rows(content)
124+
self.assertEqual(rows[0], {"id": "1", "lagging": "only-one"})
125+
self.assertEqual(rows[1], {"id": "2", "lagging": None})
126+
127+
def test_column_order_matches_csv(self):
128+
# The serialized byte stream must keep the (priority-sorted) column order so output is
129+
# reproducible - even on Python 2 where a plain dict would not - and that order must be
130+
# the SAME one CSV uses. Build the input as an OrderedDict so the expectation is fixed,
131+
# then dump the identical data as both JSONL and CSV and compare the column sequences.
132+
def table():
133+
tv = OrderedDict()
134+
tv["__infos__"] = {"count": 1, "db": "testdb", "table": "t"}
135+
tv["zebra"] = {"length": 1, "values": ["1"]}
136+
tv["alpha"] = {"length": 1, "values": ["2"]}
137+
tv["middle"] = {"length": 1, "values": ["3"]}
138+
return tv
139+
140+
jsonl_line = [l for l in self._dump(table()).splitlines() if l.strip()][0]
141+
jsonl_order = [k for k, _ in json.loads(jsonl_line, object_pairs_hook=lambda p: p)]
142+
143+
conf.dumpFormat = DUMP_FORMAT.CSV
144+
csv_path = os.path.join(self.tmp, "testdb", "t.csv")
145+
if os.path.exists(csv_path):
146+
os.remove(csv_path)
147+
self.d.dbTableValues(table())
148+
with open(csv_path) as f:
149+
csv_header = f.read().splitlines()[0]
150+
csv_order = [c.strip() for c in csv_header.split(conf.csvDel)]
151+
152+
self.assertEqual(jsonl_order, csv_order)
153+
154+
def test_unicode_value_not_escaped(self):
155+
# ensure_ascii=False keeps multibyte data readable; it must round-trip through json.loads
156+
content = self._dump({
157+
"__infos__": {"count": 1, "db": "testdb", "table": "t"},
158+
"name": {"length": 6, "values": [u"\u0107evap"]},
159+
})
160+
self.assertEqual(self._rows(content)[0]["name"], u"\u0107evap")
161+
162+
163+
if __name__ == "__main__":
164+
unittest.main()

0 commit comments

Comments
 (0)