ghostbase-cli 0.1.2__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/PKG-INFO +1 -1
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/app.py +17 -8
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/db_engine.py +53 -15
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/exporter.py +11 -8
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/ghostbase_cli.egg-info/PKG-INFO +1 -1
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/pyproject.toml +1 -1
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/tests/test_vulnerabilities_fixed.py +33 -13
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/LICENSE +0 -0
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/README.md +0 -0
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/ghostbase_cli.egg-info/SOURCES.txt +0 -0
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/ghostbase_cli.egg-info/dependency_links.txt +0 -0
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/ghostbase_cli.egg-info/entry_points.txt +0 -0
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/ghostbase_cli.egg-info/requires.txt +0 -0
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/ghostbase_cli.egg-info/top_level.txt +0 -0
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/main.py +0 -0
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/setup.cfg +0 -0
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/tests/test_all.py +0 -0
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/tests/test_exhaustive.py +0 -0
- {ghostbase_cli-0.1.2 → ghostbase_cli-0.1.3}/tests/test_real_db.py +0 -0
|
@@ -342,7 +342,7 @@ class GhostBaseApp(App):
|
|
|
342
342
|
yield Label(f"[{self.db.db_type_label()}]", id="db-type-label")
|
|
343
343
|
yield Label(db_label, id="db-name-label")
|
|
344
344
|
yield Label(safe_text, id="safe-badge", classes=safe_class)
|
|
345
|
-
yield Label("v0.1.
|
|
345
|
+
yield Label("v0.1.3", id="version-label")
|
|
346
346
|
|
|
347
347
|
with Horizontal(id="main-container"):
|
|
348
348
|
with Vertical(id="sidebar"):
|
|
@@ -389,9 +389,13 @@ class GhostBaseApp(App):
|
|
|
389
389
|
# Lifecycle
|
|
390
390
|
|
|
391
391
|
async def on_mount(self):
|
|
392
|
-
# Configure database engine level Read-Only mode if safe_mode is enabled
|
|
392
|
+
# Configure database engine level Read-Only mode if safe_mode is enabled (fail-closed)
|
|
393
393
|
if self.safe_mode:
|
|
394
|
-
|
|
394
|
+
try:
|
|
395
|
+
self.db.set_read_only(True)
|
|
396
|
+
except Exception as e:
|
|
397
|
+
self.safe_mode = False
|
|
398
|
+
self.notify(f"Failed to enable Read-Only mode on database: {e}", severity="error")
|
|
395
399
|
|
|
396
400
|
tables = self.db.get_tables()
|
|
397
401
|
# Only populate demo tables when explicitly requested, in-memory, or default sample.db AND NOT in safe_mode
|
|
@@ -810,13 +814,18 @@ class GhostBaseApp(App):
|
|
|
810
814
|
self.notify("Execution Plan analyzed successfully")
|
|
811
815
|
|
|
812
816
|
def action_toggle_safe_mode(self):
|
|
813
|
-
"""Toggle Production Safe Mode (Ctrl+S)"""
|
|
814
|
-
|
|
817
|
+
"""Toggle Production Safe Mode (Ctrl+S) - Fail-closed"""
|
|
818
|
+
new_mode = not self.safe_mode
|
|
815
819
|
self._pending_destructive_query = None
|
|
816
820
|
self._pending_destructive_time = 0.0
|
|
817
821
|
|
|
818
|
-
# Enforce/revoke database session level Read-Only
|
|
819
|
-
|
|
822
|
+
# Enforce/revoke database session level Read-Only (Fail-closed)
|
|
823
|
+
try:
|
|
824
|
+
self.db.set_read_only(new_mode)
|
|
825
|
+
self.safe_mode = new_mode
|
|
826
|
+
except Exception as e:
|
|
827
|
+
self.notify(f"Cannot enable Safe Mode: {e}", severity="error", timeout=5)
|
|
828
|
+
return
|
|
820
829
|
|
|
821
830
|
badge = self.query_one("#safe-badge", Label)
|
|
822
831
|
if self.safe_mode:
|
|
@@ -905,7 +914,7 @@ class GhostBaseApp(App):
|
|
|
905
914
|
else os.getcwd()
|
|
906
915
|
)
|
|
907
916
|
out = os.path.join(db_dir, "ghostbase_export.sql")
|
|
908
|
-
Exporter.to_sql_inserts(table_name, self.last_columns, self.last_rows, out)
|
|
917
|
+
Exporter.to_sql_inserts(table_name, self.last_columns, self.last_rows, out, dialect=self.db.db_type)
|
|
909
918
|
self.notify(f"Saved: {out}", timeout=5)
|
|
910
919
|
|
|
911
920
|
# Status bar
|
|
@@ -65,20 +65,24 @@ class DatabaseEngine:
|
|
|
65
65
|
"Run: pip install pymysql"
|
|
66
66
|
)
|
|
67
67
|
parsed = urllib.parse.urlparse(url)
|
|
68
|
+
# Use SSDictCursor (Server-Side streaming cursor) to protect client memory on massive queries
|
|
68
69
|
self.conn = pymysql.connect(
|
|
69
70
|
host=parsed.hostname or "localhost",
|
|
70
71
|
port=parsed.port or 3306,
|
|
71
72
|
user=parsed.username or "root",
|
|
72
73
|
password=parsed.password or "",
|
|
73
74
|
database=parsed.path.lstrip("/"),
|
|
74
|
-
cursorclass=pymysql.cursors.
|
|
75
|
+
cursorclass=pymysql.cursors.SSDictCursor,
|
|
75
76
|
autocommit=True,
|
|
76
77
|
)
|
|
77
78
|
|
|
78
|
-
def set_read_only(self, enabled: bool):
|
|
79
|
-
"""
|
|
79
|
+
def set_read_only(self, enabled: bool) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Configure database engine session-level Read-Only mode.
|
|
82
|
+
Returns True if successful. Raises RuntimeError if enabling fails (fail-closed).
|
|
83
|
+
"""
|
|
80
84
|
if not self.conn:
|
|
81
|
-
return
|
|
85
|
+
return False
|
|
82
86
|
try:
|
|
83
87
|
cur = self.conn.cursor()
|
|
84
88
|
if self.db_type == "sqlite":
|
|
@@ -90,8 +94,11 @@ class DatabaseEngine:
|
|
|
90
94
|
elif self.db_type == "mysql":
|
|
91
95
|
mode = "READ ONLY" if enabled else "READ WRITE"
|
|
92
96
|
cur.execute(f"SET SESSION TRANSACTION {mode};")
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
return True
|
|
98
|
+
except Exception as e:
|
|
99
|
+
if enabled:
|
|
100
|
+
raise RuntimeError(f"Database engine failed to activate Read-Only mode: {e}")
|
|
101
|
+
return False
|
|
95
102
|
|
|
96
103
|
def quote_table_name(self, table_name: str) -> str:
|
|
97
104
|
"""Properly quote table identifier for queries based on database engine."""
|
|
@@ -103,6 +110,9 @@ class DatabaseEngine:
|
|
|
103
110
|
return f'"{s}"."{t}"'
|
|
104
111
|
return f'"{table_name}"'
|
|
105
112
|
elif self.db_type == "mysql":
|
|
113
|
+
if "." in table_name:
|
|
114
|
+
s, t = table_name.split(".", 1)
|
|
115
|
+
return f"`{s}`.`{t}`"
|
|
106
116
|
return f"`{table_name}`"
|
|
107
117
|
else: # sqlite
|
|
108
118
|
return f"[{table_name}]"
|
|
@@ -347,10 +357,10 @@ class DatabaseEngine:
|
|
|
347
357
|
def _mask_strings_and_literals(sql: str) -> Tuple[str, List[str]]:
|
|
348
358
|
"""
|
|
349
359
|
Extract and mask all string literals and quoted identifiers:
|
|
350
|
-
- PostgreSQL dollar-quoted strings
|
|
351
|
-
- Standard single-quoted strings
|
|
352
|
-
- Standard double-quoted strings / identifiers
|
|
353
|
-
- MySQL backtick identifiers
|
|
360
|
+
- PostgreSQL dollar-quoted strings with exact tag matching: $tag$ ... $tag$ (using backreference \2)
|
|
361
|
+
- Standard single-quoted strings: '...' (with '' escaping)
|
|
362
|
+
- Standard double-quoted strings / identifiers: "..."
|
|
363
|
+
- MySQL backtick identifiers: `...`
|
|
354
364
|
"""
|
|
355
365
|
literals = []
|
|
356
366
|
def _repl(m):
|
|
@@ -358,7 +368,13 @@ class DatabaseEngine:
|
|
|
358
368
|
literals.append(m.group(0))
|
|
359
369
|
return f" ___GB_LIT_{idx}___ "
|
|
360
370
|
|
|
361
|
-
|
|
371
|
+
# Matches:
|
|
372
|
+
# 1. Plain untagged dollar-quotes: $$...$$
|
|
373
|
+
# 2. Tagged dollar-quotes with strict matching backreference: $tag$...$tag$
|
|
374
|
+
# 3. Standard single-quoted strings: '...'
|
|
375
|
+
# 4. Standard double-quoted strings/identifiers: "..."
|
|
376
|
+
# 5. MySQL backticks: `...`
|
|
377
|
+
pattern = r"(\$\$[\s\S]*?\$\$|\$([a-zA-Z_]\w*)\$[\s\S]*?\$\2\$|'(?:''|[^'])*'|\"(?:\"\"|[^\"])*\"|`(?:``|[^`])*`)"
|
|
362
378
|
masked = re.sub(pattern, _repl, sql)
|
|
363
379
|
return masked, literals
|
|
364
380
|
|
|
@@ -367,7 +383,7 @@ class DatabaseEngine:
|
|
|
367
383
|
Check query safety.
|
|
368
384
|
- Strings and dollar-quotes are masked FIRST so inner text ('WHERE ...') does not bypass checks.
|
|
369
385
|
- Comments (-- and /* */) are stripped second.
|
|
370
|
-
- In Safe Mode: Enforces strict Read-Only operations.
|
|
386
|
+
- In Safe Mode: Enforces strict Read-Only operations, blocking all DML, DDL, DO blocks, CALL, and SET configuration changes.
|
|
371
387
|
- In Normal Mode: Intercepts unconstrained destructive queries.
|
|
372
388
|
"""
|
|
373
389
|
# 1. Mask all string literals and dollar-quoted strings FIRST
|
|
@@ -400,10 +416,15 @@ class DatabaseEngine:
|
|
|
400
416
|
(r"\bNEXTVAL\s*\(", "NEXTVAL sequence advance"),
|
|
401
417
|
(r"\bINTO\s+OUTFILE\b", "INTO OUTFILE export"),
|
|
402
418
|
(r"\bINTO\s+DUMPFILE\b", "INTO DUMPFILE export"),
|
|
419
|
+
(r"\bSET\b", "SET configuration change"),
|
|
420
|
+
(r"\bRESET\b", "RESET configuration change"),
|
|
421
|
+
(r"\bDO\b", "DO procedural block"),
|
|
422
|
+
(r"\bCALL\b", "CALL stored procedure"),
|
|
423
|
+
(r"\bMERGE\b", "MERGE statement"),
|
|
403
424
|
]
|
|
404
425
|
for pattern, desc in write_keywords:
|
|
405
426
|
if re.search(pattern, q_upper):
|
|
406
|
-
return True, f"Safe Mode is active: write operations ({desc}) are strictly blocked"
|
|
427
|
+
return True, f"Safe Mode is active: write or state-altering operations ({desc}) are strictly blocked"
|
|
407
428
|
|
|
408
429
|
if re.search(r"\bPRAGMA\b\s+\w+\s*=", q_upper):
|
|
409
430
|
return True, "Safe Mode is active: PRAGMA configuration writes are blocked"
|
|
@@ -504,16 +525,27 @@ class DatabaseEngine:
|
|
|
504
525
|
|
|
505
526
|
def execute_query(self, query: str, max_rows: int = 1000) -> Dict[str, Any]:
|
|
506
527
|
start = time.perf_counter()
|
|
528
|
+
cur = None
|
|
507
529
|
try:
|
|
508
530
|
# Check if query is DML/DDL write operation (even if it has RETURNING)
|
|
509
531
|
masked, _ = self._mask_strings_and_literals(query)
|
|
510
532
|
clean_q = re.sub(r"/\*[\s\S]*?\*/", " ", masked)
|
|
511
533
|
clean_q = re.sub(r"--[^\r\n]*", " ", clean_q).strip().upper()
|
|
512
534
|
is_write_dml = bool(
|
|
513
|
-
re.search(r"\b(INSERT\s+INTO|UPDATE\s+|DELETE\s+FROM|DELETE\s+|REPLACE\s+INTO|CREATE\s+|DROP\s+|ALTER\s+|TRUNCATE\s+)\b", clean_q)
|
|
535
|
+
re.search(r"\b(INSERT\s+INTO|UPDATE\s+|DELETE\s+FROM|DELETE\s+|REPLACE\s+INTO|CREATE\s+|DROP\s+|ALTER\s+|TRUNCATE\s+|DO\s+|CALL\s+|MERGE\s+)\b", clean_q)
|
|
514
536
|
)
|
|
515
537
|
|
|
516
|
-
|
|
538
|
+
# Use server-side streaming cursor for PostgreSQL read queries to prevent client RAM overflow
|
|
539
|
+
if self.db_type == "postgresql" and not is_write_dml:
|
|
540
|
+
stream_name = f"gb_stream_{int(time.perf_counter() * 1000000)}"
|
|
541
|
+
try:
|
|
542
|
+
cur = self.conn.cursor(name=stream_name)
|
|
543
|
+
cur.itersize = max_rows + 1
|
|
544
|
+
except Exception:
|
|
545
|
+
cur = self.conn.cursor()
|
|
546
|
+
else:
|
|
547
|
+
cur = self.conn.cursor()
|
|
548
|
+
|
|
517
549
|
cur.execute(query)
|
|
518
550
|
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
519
551
|
|
|
@@ -569,6 +601,12 @@ class DatabaseEngine:
|
|
|
569
601
|
"error": str(e),
|
|
570
602
|
"elapsed_ms": round(elapsed_ms, 2),
|
|
571
603
|
}
|
|
604
|
+
finally:
|
|
605
|
+
if cur:
|
|
606
|
+
try:
|
|
607
|
+
cur.close()
|
|
608
|
+
except Exception:
|
|
609
|
+
pass
|
|
572
610
|
|
|
573
611
|
# --- Helpers ---
|
|
574
612
|
|
|
@@ -40,11 +40,14 @@ class Exporter:
|
|
|
40
40
|
return deduped
|
|
41
41
|
|
|
42
42
|
@staticmethod
|
|
43
|
-
def _escape_ident(ident: str) -> str:
|
|
43
|
+
def _escape_ident(ident: str, dialect: str = "sqlite") -> str:
|
|
44
44
|
s = str(ident).strip()
|
|
45
|
+
q = "`" if str(dialect).lower() == "mysql" else '"'
|
|
46
|
+
escaped_q = q * 2
|
|
47
|
+
|
|
45
48
|
if "." in s:
|
|
46
49
|
parts = s.split(".")
|
|
47
|
-
return ".".join(f
|
|
50
|
+
return ".".join(f"{q}{p.replace(q, escaped_q)}{q}" for p in parts)
|
|
48
51
|
|
|
49
52
|
reserved = {
|
|
50
53
|
"ALL", "ALTER", "AND", "ANY", "AS", "ASC", "BETWEEN", "BY", "CASE",
|
|
@@ -59,8 +62,8 @@ class Exporter:
|
|
|
59
62
|
"USER", "VALUES", "VIEW", "WHEN", "WHERE", "WITH"
|
|
60
63
|
}
|
|
61
64
|
if not s.isidentifier() or s.upper() in reserved:
|
|
62
|
-
escaped = s.replace(
|
|
63
|
-
return f
|
|
65
|
+
escaped = s.replace(q, escaped_q)
|
|
66
|
+
return f"{q}{escaped}{q}"
|
|
64
67
|
return s
|
|
65
68
|
|
|
66
69
|
@staticmethod
|
|
@@ -95,13 +98,13 @@ class Exporter:
|
|
|
95
98
|
f.write("| " + " | ".join(clean_cells) + " |\n")
|
|
96
99
|
|
|
97
100
|
@staticmethod
|
|
98
|
-
def to_sql_inserts(table_name: str, columns: List[str], rows: List[List[Any]], output_path: str):
|
|
101
|
+
def to_sql_inserts(table_name: str, columns: List[str], rows: List[List[Any]], output_path: str, dialect: str = "sqlite"):
|
|
99
102
|
table = (table_name or "exported_table").strip()
|
|
100
|
-
escaped_table = Exporter._escape_ident(table)
|
|
101
|
-
escaped_cols = [Exporter._escape_ident(c) for c in columns]
|
|
103
|
+
escaped_table = Exporter._escape_ident(table, dialect=dialect)
|
|
104
|
+
escaped_cols = [Exporter._escape_ident(c, dialect=dialect) for c in columns]
|
|
102
105
|
cols_str = ", ".join(escaped_cols)
|
|
103
106
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
104
|
-
f.write(f"-- GhostBase SQL Export: {table}\n")
|
|
107
|
+
f.write(f"-- GhostBase SQL Export: {table} ({dialect})\n")
|
|
105
108
|
f.write(f"-- Total: {len(rows)} row(s)\n\n")
|
|
106
109
|
for r in rows:
|
|
107
110
|
vals = []
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ghostbase-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "A fast, keyboard-driven terminal database studio for SQLite, PostgreSQL & MySQL"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "4yk5", email = "contact.4yk5@proton.me" }]
|
|
@@ -41,6 +41,11 @@ def test_safe_mode_and_comment_bypass():
|
|
|
41
41
|
is_dest, r = db.check_safety(dollar_bypass, is_safe_mode=False)
|
|
42
42
|
assert is_dest, f"VULNERABILITY: Dollar-quoted string containing WHERE fooled check_safety: {r}"
|
|
43
43
|
|
|
44
|
+
# Postgres dollar-quoted string with tag mismatch ($tag$ vs $other$)
|
|
45
|
+
mismatched_tag_query = "UPDATE users SET role = $tag$abc $other$ WHERE marker $tag$;"
|
|
46
|
+
is_dest, r = db.check_safety(mismatched_tag_query, is_safe_mode=False)
|
|
47
|
+
assert is_dest, f"VULNERABILITY: Dollar-quoted tag mismatch bypassed WHERE check: {r}"
|
|
48
|
+
|
|
44
49
|
# Safe Mode STRICT read-only checks
|
|
45
50
|
safe_queries = [
|
|
46
51
|
"SELECT * FROM users WHERE id = 5;",
|
|
@@ -52,6 +57,7 @@ def test_safe_mode_and_comment_bypass():
|
|
|
52
57
|
is_dest, r = db.check_safety(q, is_safe_mode=True)
|
|
53
58
|
assert not is_dest, f"Safe query wrongly blocked in Safe Mode: {q} ({r})"
|
|
54
59
|
|
|
60
|
+
# Safe Mode must strictly block DML, DDL, DO blocks, CALL, MERGE, and SET changes
|
|
55
61
|
unsafe_queries = [
|
|
56
62
|
("UPDATE users SET role = 'Admin' WHERE id = 5;", "UPDATE"),
|
|
57
63
|
("INSERT INTO users (name) VALUES ('Test');", "INSERT"),
|
|
@@ -62,6 +68,11 @@ def test_safe_mode_and_comment_bypass():
|
|
|
62
68
|
("CREATE TABLE test (id INT);", "CREATE"),
|
|
63
69
|
("SELECT nextval('users_id_seq');", "NEXTVAL"),
|
|
64
70
|
("SELECT * INTO OUTFILE '/tmp/f' FROM users;", "INTO OUTFILE"),
|
|
71
|
+
("SET default_transaction_read_only = OFF;", "SET"),
|
|
72
|
+
("RESET ALL;", "RESET"),
|
|
73
|
+
("DO $body$ BEGIN UPDATE users SET role = 'Admin'; END $body$;", "DO"),
|
|
74
|
+
("CALL custom_proc();", "CALL"),
|
|
75
|
+
("MERGE INTO target USING source ON target.id = source.id WHEN MATCHED THEN DELETE;", "MERGE"),
|
|
65
76
|
]
|
|
66
77
|
for q, op in unsafe_queries:
|
|
67
78
|
is_dest, r = db.check_safety(q, is_safe_mode=True)
|
|
@@ -72,7 +83,7 @@ def test_safe_mode_and_comment_bypass():
|
|
|
72
83
|
|
|
73
84
|
|
|
74
85
|
def test_database_engine_level_readonly():
|
|
75
|
-
print("[*] 2. Testing Database Engine-Level Read-Only Enforcement...")
|
|
86
|
+
print("[*] 2. Testing Database Engine-Level Read-Only & Fail-Closed Enforcement...")
|
|
76
87
|
db = DatabaseEngine(":memory:")
|
|
77
88
|
db.conn.execute("CREATE TABLE read_only_test (id INT, val TEXT);")
|
|
78
89
|
db.conn.commit()
|
|
@@ -180,11 +191,12 @@ def test_returning_dml_classification():
|
|
|
180
191
|
print(" [+] DML with RETURNING Classification: PASS")
|
|
181
192
|
|
|
182
193
|
|
|
183
|
-
def
|
|
184
|
-
print("[*] 7. Testing Exporter JSON Types & SQL
|
|
194
|
+
def test_exporter_json_types_and_dialect_sql():
|
|
195
|
+
print("[*] 7. Testing Exporter JSON Types & Dialect-Aware SQL Escaping (MySQL vs Postgres/SQLite)...")
|
|
185
196
|
tmp = tempfile.gettempdir()
|
|
186
197
|
json_path = os.path.join(tmp, "test_types.json")
|
|
187
|
-
|
|
198
|
+
sql_pg_path = os.path.join(tmp, "test_pg.sql")
|
|
199
|
+
sql_my_path = os.path.join(tmp, "test_mysql.sql")
|
|
188
200
|
|
|
189
201
|
# JSON export with Decimal, datetime, UUID, bytes, and duplicate column names
|
|
190
202
|
cols = ["id", "id", "price", "created", "uid", "raw"]
|
|
@@ -206,17 +218,24 @@ def test_exporter_dedup_and_ident_escaping():
|
|
|
206
218
|
assert data[0]["uid"] == "12345678-1234-5678-1234-567812345678"
|
|
207
219
|
assert data[0]["raw"] == "binary_ghost"
|
|
208
220
|
|
|
209
|
-
#
|
|
221
|
+
# PostgreSQL / SQLite dialect SQL export (Double quotes)
|
|
210
222
|
table_name = "analytics.order"
|
|
211
223
|
cols_special = ["first name", "group", "price"]
|
|
212
224
|
rows_special = [["Bob", "Admin", decimal.Decimal("10.50")]]
|
|
213
|
-
Exporter.to_sql_inserts(table_name, cols_special, rows_special,
|
|
214
|
-
with open(
|
|
215
|
-
|
|
225
|
+
Exporter.to_sql_inserts(table_name, cols_special, rows_special, sql_pg_path, dialect="postgresql")
|
|
226
|
+
with open(sql_pg_path, "r", encoding="utf-8") as f:
|
|
227
|
+
pg_content = f.read()
|
|
228
|
+
|
|
229
|
+
assert 'INSERT INTO "analytics"."order" ("first name", "group", price) VALUES (\'Bob\', \'Admin\', 10.50);' in pg_content
|
|
230
|
+
|
|
231
|
+
# MySQL dialect SQL export (Backticks)
|
|
232
|
+
Exporter.to_sql_inserts(table_name, cols_special, rows_special, sql_my_path, dialect="mysql")
|
|
233
|
+
with open(sql_my_path, "r", encoding="utf-8") as f:
|
|
234
|
+
my_content = f.read()
|
|
216
235
|
|
|
217
|
-
assert 'INSERT INTO
|
|
236
|
+
assert 'INSERT INTO `analytics`.`order` (`first name`, `group`, price) VALUES (\'Bob\', \'Admin\', 10.50);' in my_content
|
|
218
237
|
|
|
219
|
-
print(" [+] Exporter JSON Types & SQL Escaping: PASS")
|
|
238
|
+
print(" [+] Exporter JSON Types & Dialect-Aware SQL Escaping: PASS")
|
|
220
239
|
|
|
221
240
|
|
|
222
241
|
def test_table_quoting_and_cte_export_detection():
|
|
@@ -232,6 +251,7 @@ def test_table_quoting_and_cte_export_detection():
|
|
|
232
251
|
db_my = DatabaseEngine.__new__(DatabaseEngine)
|
|
233
252
|
db_my.db_type = "mysql"
|
|
234
253
|
assert db_my.quote_table_name("order") == "`order`"
|
|
254
|
+
assert db_my.quote_table_name("analytics.users") == "`analytics`.`users`"
|
|
235
255
|
|
|
236
256
|
# CTE target detection test
|
|
237
257
|
cte_query = """
|
|
@@ -266,7 +286,7 @@ def test_result_set_memory_limit():
|
|
|
266
286
|
|
|
267
287
|
def main():
|
|
268
288
|
print("\n" + "=" * 60)
|
|
269
|
-
print("RUNNING EXHAUSTIVE REFINED VERIFICATION TESTS (v0.1.
|
|
289
|
+
print("RUNNING EXHAUSTIVE REFINED VERIFICATION TESTS (v0.1.3)")
|
|
270
290
|
print("=" * 60)
|
|
271
291
|
|
|
272
292
|
test_safe_mode_and_comment_bypass()
|
|
@@ -275,12 +295,12 @@ def main():
|
|
|
275
295
|
test_sqlite_foreign_keys_enforced()
|
|
276
296
|
test_empty_sqlite_not_polluted()
|
|
277
297
|
test_returning_dml_classification()
|
|
278
|
-
|
|
298
|
+
test_exporter_json_types_and_dialect_sql()
|
|
279
299
|
test_table_quoting_and_cte_export_detection()
|
|
280
300
|
test_result_set_memory_limit()
|
|
281
301
|
|
|
282
302
|
print("\n" + "=" * 60)
|
|
283
|
-
print("ALL
|
|
303
|
+
print("ALL ANALYST VULNERABILITIES VERIFIED AND 100% PASSING!")
|
|
284
304
|
print("=" * 60 + "\n")
|
|
285
305
|
|
|
286
306
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|