Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding ability to select SORTABLE_HTML as dump format to have sortable tables in HTML dumps. #5825

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/core/dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
from lib.core.replication import Replication
from lib.core.settings import DUMP_FILE_BUFFER_SIZE
from lib.core.settings import HTML_DUMP_CSS_STYLE
from lib.core.settings import HTML_DUMP_CSS_SORTABLE_STYLE
from lib.core.settings import HTML_DUMP_SORTABLE_JAVASCRIPT
from lib.core.settings import IS_WIN
from lib.core.settings import METADB_SUFFIX
from lib.core.settings import MIN_BINARY_DISK_DUMP_SIZE
Expand Down Expand Up @@ -541,6 +543,9 @@ def dbTableValues(self, tableValues):
dataToDumpFile(dumpFP, "<meta name=\"generator\" content=\"%s\" />\n" % VERSION_STRING)
dataToDumpFile(dumpFP, "<title>%s</title>\n" % ("%s%s" % ("%s." % db if METADB_SUFFIX not in db else "", table)))
dataToDumpFile(dumpFP, HTML_DUMP_CSS_STYLE)
if conf.dumpSortable:
dataToDumpFile(dumpFP, HTML_DUMP_CSS_SORTABLE_STYLE)
dataToDumpFile(dumpFP, HTML_DUMP_SORTABLE_JAVASCRIPT)
dataToDumpFile(dumpFP, "\n</head>\n<body>\n<table>\n<thead>\n<tr>\n")

if count == 1:
Expand Down
1 change: 1 addition & 0 deletions lib/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ class REGISTRY_OPERATION(object):
class DUMP_FORMAT(object):
CSV = "CSV"
HTML = "HTML"
SORTABLE_HTML = "SORTABLE_HTML"
SQLITE = "SQLITE"

class HTTP_HEADER(object):
Expand Down
160 changes: 147 additions & 13 deletions lib/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,29 +918,163 @@

# CSS style used in HTML dump format
HTML_DUMP_CSS_STYLE = """<style>
table{
margin:10;
background-color:#FFFFFF;
font-family:verdana;
font-size:12px;
align:center;
table {
margin: 10px;
background: #fff;
font: 12px verdana;
text-align: center;
}
thead{
font-weight:bold;
background-color:#4F81BD;
color:#FFFFFF;
color: #fff;
}
tr:nth-child(even) {
background-color: #D3DFEE
background-color: #D3DFEE;
}
td{
font-size:12px;
</style>"""

HTML_DUMP_CSS_SORTABLE_STYLE = """
<style>
table thead th {
cursor: pointer;
white-space: nowrap;
position: sticky;
top: 0;
z-index: 1;
}
th{
font-size:12px;

table thead th::after,
table thead th::before {
color: transparent;
}

table thead th::after {
margin-left: 3px;
content: "▸";
}

table thead th:hover::after,
table thead th[aria-sort]::after {
color: inherit;
}

table thead th[aria-sort=descending]::after {
content: "▾";
}
</style>"""

table thead th[aria-sort=ascending]::after {
content: "▴";
}

table thead th.indicator-left::before {
margin-right: 3px;
content: "▸";
}

table thead th.indicator-left[aria-sort=descending]::before {
color: inherit;
content: "▾";
}

table thead th.indicator-left[aria-sort=ascending]::before {
color: inherit;
content: "▴";
}
</style>
"""
HTML_DUMP_SORTABLE_JAVASCRIPT = """<script>
window.addEventListener('DOMContentLoaded', () => {
document.addEventListener('click', event => {
try {
const isAltSort = event.shiftKey || event.altKey;

// Find the clicked table header
const findParentElement = (element, nodeName) =>
element.nodeName === nodeName ? element : findParentElement(element.parentNode, nodeName);

const headerCell = findParentElement(event.target, 'TH');
const headerRow = headerCell.parentNode;
const thead = headerRow.parentNode;
const table = thead.parentNode;

if (thead.nodeName !== 'THEAD') return;

// Reset sort indicators on other headers
Array.from(headerRow.cells).forEach(cell => {
if (cell !== headerCell) cell.removeAttribute('aria-sort');
});

// Toggle sort direction
const currentSort = headerCell.getAttribute('aria-sort');
const isAscending = table.classList.contains('asc') && currentSort !== 'ascending';
const sortDirection = (currentSort === 'descending' || isAscending) ? 'ascending' : 'descending';
headerCell.setAttribute('aria-sort', sortDirection);

// Debounce sort operation
if (table.dataset.timer) clearTimeout(Number(table.dataset.timer));

table.dataset.timer = setTimeout(() => {
sortTable(table, isAltSort);
}, 1).toString();
} catch (error) {
console.error('Sorting error:', error);
}
});
});

function sortTable(table, useAltSort) {
table.dispatchEvent(new CustomEvent('sort-start', { bubbles: true }));

const sortHeader = table.tHead.querySelector('th[aria-sort]');
const headerRow = table.tHead.children[0];
const isAscending = sortHeader.getAttribute('aria-sort') === 'ascending';
const shouldPushEmpty = table.classList.contains('n-last');
const sortColumnIndex = Number(sortHeader.dataset.sortCol ?? sortHeader.cellIndex);

const getCellValue = cell => {
if (useAltSort) return cell.dataset.sortAlt;
return cell.dataset.sort ?? cell.textContent;
};

const compareRows = (row1, row2) => {
const value1 = getCellValue(row1.cells[sortColumnIndex]);
const value2 = getCellValue(row2.cells[sortColumnIndex]);

// Handle empty values
if (shouldPushEmpty) {
if (value1 === '' && value2 !== '') return -1;
if (value2 === '' && value1 !== '') return 1;
}

// Compare numerically if possible, otherwise use string comparison
const numericDiff = Number(value1) - Number(value2);
const comparison = isNaN(numericDiff) ?
value1.localeCompare(value2, undefined, { numeric: true }) :
numericDiff;

// Handle tiebreaker
if (comparison === 0 && headerRow.cells[sortColumnIndex]?.dataset.sortTbr) {
const tiebreakIndex = Number(headerRow.cells[sortColumnIndex].dataset.sortTbr);
return compareRows(row1, row2, tiebreakIndex);
}

return isAscending ? -comparison : comparison;
};

// Sort each tbody
Array.from(table.tBodies).forEach(tbody => {
const rows = Array.from(tbody.rows);
const sortedRows = rows.sort(compareRows);

const newTbody = tbody.cloneNode();
newTbody.append(...sortedRows);
tbody.replaceWith(newTbody);
});

table.dispatchEvent(new CustomEvent('sort-end', { bubbles: true }));
}
</script>"""
# Leaving (dirty) possibility to change values from here (e.g. `export SQLMAP__MAX_NUMBER_OF_THREADS=20`)
for key, value in os.environ.items():
if key.upper().startswith("%s_" % SQLMAP_ENVIRONMENT_PREFIX):
Expand Down
2 changes: 1 addition & 1 deletion lib/parse/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ def cmdLineParser(argv=None):
help="Store dumped data to a custom file")

general.add_argument("--dump-format", dest="dumpFormat",
help="Format of dumped data (CSV (default), HTML or SQLITE)")
help="Format of dumped data (CSV (default), HTML, SORTABLE_HTML or SQLITE)")

general.add_argument("--encoding", dest="encoding",
help="Character encoding used for data retrieval (e.g. GBK)")
Expand Down
4 changes: 3 additions & 1 deletion sqlmap.conf
Original file line number Diff line number Diff line change
Expand Up @@ -754,9 +754,11 @@ csvDel = ,
dumpFile =

# Format of dumped data
# Valid: CSV, HTML or SQLITE
# Valid: CSV, HTML, SORTABLE_HTML or SQLITE
dumpFormat = CSV

dumpSortable = False

# Force character encoding used for data retrieval.
encoding =

Expand Down
6 changes: 6 additions & 0 deletions sqlmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ def main():
if checkPipedInput():
conf.batch = True

if conf.get("dumpFormat") == "SORTABLE_HTML":
conf.dumpFormat = "HTML"
conf.dumpSortable = True
else:
conf.dumpSortable = False

if conf.get("api"):
# heavy imports
from lib.utils.api import StdDbOut
Expand Down
Loading