dsh-switchman 1.3.0
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.
- package/LICENSE +21 -0
- package/README.md +79 -0
- package/README.zh.md +79 -0
- package/client.js +2441 -0
- package/cordis.patch.yml +599 -0
- package/host/config.js +116 -0
- package/host/context-watch.js +620 -0
- package/host/data/capability-default.json +1097 -0
- package/host/dispatch.js +205 -0
- package/host/lang.js +182 -0
- package/host/lib/rank.js +187 -0
- package/host/routes.js +398 -0
- package/icon.svg +7 -0
- package/index.js +154 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +70 -0
- package/skills/db-query/SKILL.md +322 -0
- package/skills/db-query/package-lock.json +251 -0
- package/skills/db-query/package.json +20 -0
- package/skills/db-query/references/mysql.md +137 -0
- package/skills/db-query/references/redis.md +213 -0
- package/skills/db-query/references/security.md +176 -0
- package/skills/db-query/scripts/lib/cli.js +118 -0
- package/skills/db-query/scripts/lib/config.js +215 -0
- package/skills/db-query/scripts/lib/mysql-readonly.js +655 -0
- package/skills/db-query/scripts/lib/output.js +322 -0
- package/skills/db-query/scripts/lib/redis-readonly.js +362 -0
- package/skills/db-query/scripts/mysql-query.js +198 -0
- package/skills/db-query/scripts/redis-query.js +181 -0
- package/skills/db-query/scripts/setup.sh +51 -0
- package/skills/git-commit-message/SKILL.md +49 -0
- package/skills/requirement-docs/SKILL.md +38 -0
- package/skills/requirement-docs/references/templates.md +187 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<!-- [2026-09-05]-[translated to English (skill is now English-only)]-[content unchanged] -->
|
|
2
|
+
# MySQL Syntax Matrix & Query Recipes
|
|
3
|
+
|
|
4
|
+
## Allowed / Rejected Statement Matrix
|
|
5
|
+
|
|
6
|
+
### ✅ Allowed
|
|
7
|
+
|
|
8
|
+
| Statement type | Example | Notes |
|
|
9
|
+
|----------|------|------|
|
|
10
|
+
| `SELECT` | `SELECT id, name FROM users WHERE id = 42` | Most common |
|
|
11
|
+
| `WITH...SELECT` | `WITH c AS (SELECT 1) SELECT * FROM c` | CTE supported |
|
|
12
|
+
| `SHOW` | `SHOW TABLES`, `SHOW CREATE TABLE t` | Metadata queries |
|
|
13
|
+
| `DESCRIBE` / `DESC` | `DESC users`, `DESCRIBE users name` | Table structure |
|
|
14
|
+
| `EXPLAIN SELECT` | `EXPLAIN SELECT * FROM users` | EXPLAIN SELECT only, no ANALYZE |
|
|
15
|
+
|
|
16
|
+
### ❌ Rejected
|
|
17
|
+
|
|
18
|
+
| Statement type | Rejection reason |
|
|
19
|
+
|----------|----------|
|
|
20
|
+
| `INSERT / UPDATE / DELETE / REPLACE` | Write operations |
|
|
21
|
+
| `DROP / ALTER / CREATE / TRUNCATE / RENAME` | DDL |
|
|
22
|
+
| `GRANT / REVOKE` | Privilege management |
|
|
23
|
+
| `SET` | Session variable changes |
|
|
24
|
+
| `LOCK / UNLOCK TABLES` | Explicit locks |
|
|
25
|
+
| `CALL / EXECUTE` | Stored procedures |
|
|
26
|
+
| `LOAD DATA / INTO OUTFILE` | File operations |
|
|
27
|
+
| `START TRANSACTION / COMMIT / ROLLBACK` | Transaction control is managed by the script |
|
|
28
|
+
| `HANDLER` | Low-level table access interface |
|
|
29
|
+
| All comments (`#`, `-- `, `/* */`, `/*! */`, `/*+ */`) | Can nest malicious SQL |
|
|
30
|
+
| Backslash escapes (`\`) | Can craft dangerous syntax |
|
|
31
|
+
| Multiple statements (`;` separated) | Prevents concatenation injection |
|
|
32
|
+
| `FOR UPDATE` / `LOCK IN SHARE MODE` | Explicit locking |
|
|
33
|
+
| `SLEEP()` / `BENCHMARK()` / `GET_LOCK()` | Potential DoS |
|
|
34
|
+
| `INTO OUTFILE` | Server-side file writes |
|
|
35
|
+
| `EXPLAIN ANALYZE` | Actually executes the query; blocked as a security trade-off |
|
|
36
|
+
|
|
37
|
+
### Common "legal" forms that get rejected, and alternatives
|
|
38
|
+
|
|
39
|
+
| Intent | Rejected form | Alternative |
|
|
40
|
+
|--------|-------------|----------|
|
|
41
|
+
| Query SQL with comments | `SELECT 1 /* comment */` | Remove the comment |
|
|
42
|
+
| Backticks + trailing comment | `` SELECT `id` FROM `t` -- comment `` | Remove the comment |
|
|
43
|
+
| Dynamic LIMIT (variables) | `PREPARE` + `EXECUTE` | Write a literal number |
|
|
44
|
+
| Execution analysis | `EXPLAIN ANALYZE SELECT ...` | Use `EXPLAIN SELECT ...` (no execution) |
|
|
45
|
+
| Multiple statements at once | `SELECT 1; SELECT 2` | Split into separate calls |
|
|
46
|
+
| Write inside a subquery | `SELECT (UPDATE ...)` | Not allowed; all writes are refused |
|
|
47
|
+
|
|
48
|
+
## Common read-only recipes
|
|
49
|
+
|
|
50
|
+
### Single record by primary key
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
./scripts/mysql-query.js --user=reader --database=mydb \
|
|
54
|
+
'SELECT id, username, email, status FROM users WHERE id = 42'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Recent changes by time range
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
./scripts/mysql-query.js --user=reader --database=mydb --max-rows=50 \
|
|
61
|
+
"SELECT id, status, updated_at FROM orders WHERE updated_at >= '2024-06-01 00:00:00' ORDER BY updated_at DESC"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Aggregate counts (grouped by status)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
./scripts/mysql-query.js --user=reader --database=mydb \
|
|
68
|
+
'SELECT status, COUNT(*) AS cnt FROM users GROUP BY status ORDER BY cnt DESC'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### JOIN to verify related data
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Verify order ↔ user consistency
|
|
75
|
+
./scripts/mysql-query.js --user=reader --database=mydb --max-rows=30 \
|
|
76
|
+
'SELECT o.id AS order_id, o.user_id, u.username, o.amount FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status = "pending" ORDER BY o.created_at DESC'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Execution plan
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
./scripts/mysql-query.js --user=reader --database=mydb \
|
|
83
|
+
'EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND created_at >= "2024-01-01"'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Table structure
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
./scripts/mysql-query.js --user=reader --database=mydb 'DESCRIBE orders'
|
|
90
|
+
|
|
91
|
+
# or use SHOW
|
|
92
|
+
./scripts/mysql-query.js --user=reader --database=mydb 'SHOW CREATE TABLE orders'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Connectivity test
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
./scripts/mysql-query.js --user=reader --database=mydb 'SELECT 1'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## LIMIT & truncation behavior
|
|
102
|
+
|
|
103
|
+
| Scenario | Behavior |
|
|
104
|
+
|------|------|
|
|
105
|
+
| SQL has no `LIMIT` clause | Script auto-injects `LIMIT <max-rows>` (default 200) |
|
|
106
|
+
| SQL has `LIMIT n` with `n <= max-rows` | Executed as-is |
|
|
107
|
+
| SQL has `LIMIT n` with `n > max-rows` | Validation rejects it: LIMIT exceeds the cap |
|
|
108
|
+
| Actual rows reach `max-rows` | Trailing notice "result truncated"; refine the query and re-run |
|
|
109
|
+
|
|
110
|
+
> ⚠️ The auto-injected LIMIT applies to the outermost query. Subqueries and UNION internals are unaffected (the outer LIMIT still protects).
|
|
111
|
+
|
|
112
|
+
## Field type output conventions
|
|
113
|
+
|
|
114
|
+
| MySQL type | Output form | Notes |
|
|
115
|
+
|------------|----------|------|
|
|
116
|
+
| INT / TINYINT / SMALLINT / MEDIUMINT | Number | Normal range |
|
|
117
|
+
| BIGINT | String | Avoids exceeding JS Number.MAX_SAFE_INTEGER |
|
|
118
|
+
| DECIMAL / NUMERIC | String | Avoids floating-point precision loss |
|
|
119
|
+
| FLOAT / DOUBLE | Number | Precision loss possible |
|
|
120
|
+
| DATE / DATETIME / TIMESTAMP | String | `dateStrings` mode, no timezone conversion |
|
|
121
|
+
| CHAR / VARCHAR / TEXT | String | As-is |
|
|
122
|
+
| JSON | Formatted string | Pretty-printed after JSON.parse |
|
|
123
|
+
| BLOB / BINARY | Base64 string | Binary-safe representation |
|
|
124
|
+
| NULL | `null` | null in JSON, NULL in table output |
|
|
125
|
+
|
|
126
|
+
## Offline validation
|
|
127
|
+
|
|
128
|
+
Use `--validate-only` to check SQL safety without connecting:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Validate first, then execute
|
|
132
|
+
./scripts/mysql-query.js --validate-only 'SELECT id FROM users WHERE id = 42'
|
|
133
|
+
# Once it passes, drop --validate-only and execute
|
|
134
|
+
./scripts/mysql-query.js --user=reader --database=mydb 'SELECT id FROM users WHERE id = 42'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Validation covers: statement type allowlist, comments/backslash/multi-statement/dangerous functions, and LIMIT compliance.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<!-- [2026-09-05]-[translated to English (skill is now English-only)]-[content unchanged] -->
|
|
2
|
+
# Redis Command Whitelist & Query Patterns
|
|
3
|
+
|
|
4
|
+
## Full command-args-return reference
|
|
5
|
+
|
|
6
|
+
### General / key operations
|
|
7
|
+
|
|
8
|
+
| Command | Args | Return form | Notes |
|
|
9
|
+
|------|------|----------|------|
|
|
10
|
+
| `PING` | none | `PONG` (string) | Connectivity test |
|
|
11
|
+
| `GET key` | key | String value or `(nil)` | Read a String value |
|
|
12
|
+
| `MGET key [key ...]` | 1+ keys | Array of values (in order) | Batch read Strings |
|
|
13
|
+
| `EXISTS key [key ...]` | 1+ keys | Count of existing keys | Check key existence |
|
|
14
|
+
| `TYPE key` | key | Type string | Key's data type |
|
|
15
|
+
| `TTL key` | key | Remaining seconds (integer) | Time until expiry |
|
|
16
|
+
| `PTTL key` | key | Remaining milliseconds (integer) | Expiry in ms precision |
|
|
17
|
+
| `STRLEN key` | key | String length | Byte length of String value |
|
|
18
|
+
| `SCAN cursor [MATCH pattern] [COUNT count]` | cursor, optional args | Cursor + key list | Incremental key iteration |
|
|
19
|
+
| `KEYS pattern`† | pattern | Key list | Full key match (blocking risk) |
|
|
20
|
+
| `DBSIZE` | none | Total key count | Key count in current db |
|
|
21
|
+
| `TIME` | none | Unix timestamp (sec.µs) | Server current time |
|
|
22
|
+
|
|
23
|
+
### Hash operations
|
|
24
|
+
|
|
25
|
+
| Command | Args | Return form | Notes |
|
|
26
|
+
|------|------|----------|------|
|
|
27
|
+
| `HGET key field` | key, field | Field value or `(nil)` | Read a single field |
|
|
28
|
+
| `HMGET key field [field ...]` | key, 1+ fields | Array of values | Batch read fields |
|
|
29
|
+
| `HEXISTS key field` | key, field | 1 (exists) / 0 (missing) | Check field existence |
|
|
30
|
+
| `HLEN key` | key | Field count | Number of hash fields |
|
|
31
|
+
| `HGETALL key`† | key | Alternating field-value array | Full dump (blocking risk) |
|
|
32
|
+
| `HKEYS key`† | key | Field name array | All field names (blocking risk) |
|
|
33
|
+
| `HVALS key`† | key | Value array | All values (blocking risk) |
|
|
34
|
+
| `HSCAN key cursor [MATCH pattern] [COUNT count]` | key, cursor, optional | Cursor + field-value pairs | Incremental hash iteration |
|
|
35
|
+
|
|
36
|
+
### List operations
|
|
37
|
+
|
|
38
|
+
| Command | Args | Return form | Notes |
|
|
39
|
+
|------|------|----------|------|
|
|
40
|
+
| `LINDEX key index` | key, index | Element or `(nil)` | Element by index |
|
|
41
|
+
| `LLEN key` | key | List length | Element count |
|
|
42
|
+
| `LRANGE key start stop` | key, start/stop indexes | Element array | Elements in range |
|
|
43
|
+
|
|
44
|
+
### Set operations
|
|
45
|
+
|
|
46
|
+
| Command | Args | Return form | Notes |
|
|
47
|
+
|------|------|----------|------|
|
|
48
|
+
| `SISMEMBER key member` | key, member | 1 (member) / 0 (not) | Membership check |
|
|
49
|
+
| `SCARD key` | key | Member count | Set size |
|
|
50
|
+
| `SMEMBERS key`† | key | Member array | Full dump (blocking risk) |
|
|
51
|
+
| `SSCAN key cursor [MATCH pattern] [COUNT count]` | key, cursor, optional | Cursor + member list | Incremental set iteration |
|
|
52
|
+
|
|
53
|
+
### ZSet operations
|
|
54
|
+
|
|
55
|
+
| Command | Args | Return form | Notes |
|
|
56
|
+
|------|------|----------|------|
|
|
57
|
+
| `ZCARD key` | key | Member count | ZSet size |
|
|
58
|
+
| `ZCOUNT key min max` | key, score range | Count | Count by score range |
|
|
59
|
+
| `ZSCORE key member` | key, member | Score or `(nil)` | Member's score |
|
|
60
|
+
| `ZRANK key member` | key, member | Rank or `(nil)` | Ascending rank (0-based) |
|
|
61
|
+
| `ZREVRANK key member` | key, member | Rank or `(nil)` | Descending rank (0-based) |
|
|
62
|
+
| `ZRANGE key start stop [WITHSCORES]` | key, start/stop, optional | Member (+score) array | Query by rank range |
|
|
63
|
+
| `ZRANGEBYSCORE key min max [WITHSCORES]` | key, score range, optional | Member (+score) array | Query by score range |
|
|
64
|
+
| `ZSCAN key cursor [MATCH pattern] [COUNT count]` | key, cursor, optional | Cursor + member-score pairs | Incremental zset iteration |
|
|
65
|
+
|
|
66
|
+
### Server info
|
|
67
|
+
|
|
68
|
+
| Command | Args | Return form | Notes |
|
|
69
|
+
|------|------|----------|------|
|
|
70
|
+
| `INFO [section]` | optional section | Info text block | Server info & stats |
|
|
71
|
+
| `CLIENT LIST` | none | Client connection list | Current connections |
|
|
72
|
+
| `CONFIG GET parameter`† | Parameter name | Parameter value | Read config (safe items only) |
|
|
73
|
+
|
|
74
|
+
> † Requires the `--allow-unbounded` flag.
|
|
75
|
+
|
|
76
|
+
## SCAN-family cursor usage
|
|
77
|
+
|
|
78
|
+
SCAN-family commands iterate incrementally with a cursor and never block Redis.
|
|
79
|
+
|
|
80
|
+
### Basic pattern
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# SCAN instead of KEYS to iterate keys
|
|
84
|
+
./scripts/redis-query.js -- SCAN 0 MATCH user:* COUNT 100
|
|
85
|
+
# Returns: next cursor + key list
|
|
86
|
+
# Cursor 0 means iteration is complete
|
|
87
|
+
|
|
88
|
+
# Multi-round iteration (manual loop)
|
|
89
|
+
./scripts/redis-query.js -- SCAN 0 MATCH user:* COUNT 100
|
|
90
|
+
# Returns cursor=42, keys=[user:1, user:2, ...]
|
|
91
|
+
./scripts/redis-query.js -- SCAN 42 MATCH user:* COUNT 100
|
|
92
|
+
# Returns cursor=0 (done)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Per-type SCAN
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Incremental hash iteration
|
|
99
|
+
./scripts/redis-query.js -- HSCAN session:abc 0 COUNT 50
|
|
100
|
+
./scripts/redis-query.js -- HSCAN session:abc 128 COUNT 50 # next round
|
|
101
|
+
|
|
102
|
+
# Incremental set iteration
|
|
103
|
+
./scripts/redis-query.js -- SSCAN online:users 0 COUNT 100
|
|
104
|
+
|
|
105
|
+
# Incremental zset iteration
|
|
106
|
+
./scripts/redis-query.js -- ZSCAN leaderboard 0 COUNT 100
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### SCAN caveats
|
|
110
|
+
|
|
111
|
+
- `COUNT` is a hint; actual batch sizes may differ (Redis internals).
|
|
112
|
+
- Cursor `0` means done, but keys added/removed during iteration may be missed or repeated.
|
|
113
|
+
- `MATCH` filters server-side: it reduces network transfer, not traversal work.
|
|
114
|
+
|
|
115
|
+
## TTL / PTTL return values
|
|
116
|
+
|
|
117
|
+
| Return | Meaning | Notes |
|
|
118
|
+
|--------|------|------|
|
|
119
|
+
| `>= 0` | Remaining lifetime | TTL in seconds, PTTL in milliseconds |
|
|
120
|
+
| `-1` | No expiry | Key exists without a TTL set |
|
|
121
|
+
| `-2` | Key missing | Expired and removed, or never created |
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
./scripts/redis-query.js -- TTL cache:homepage
|
|
125
|
+
# 300 → expires in 5 minutes
|
|
126
|
+
# -1 → no expiry
|
|
127
|
+
# -2 → key missing
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Safe query steps per data type
|
|
131
|
+
|
|
132
|
+
Recommended pattern: **run `TYPE` first, then pick the matching command**.
|
|
133
|
+
|
|
134
|
+
### String
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
./scripts/redis-query.js -- TYPE user:42 # confirm it's a string
|
|
138
|
+
./scripts/redis-query.js -- GET user:42 # read value
|
|
139
|
+
./scripts/redis-query.js -- TTL user:42 # check expiry
|
|
140
|
+
./scripts/redis-query.js -- STRLEN user:42 # check length
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Hash
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
./scripts/redis-query.js -- TYPE user:42 # confirm it's a hash
|
|
147
|
+
./scripts/redis-query.js -- HLEN user:42 # field count (size hint)
|
|
148
|
+
./scripts/redis-query.js -- HGET user:42 name # single field
|
|
149
|
+
./scripts/redis-query.js -- HMGET user:42 name email status # batch fields
|
|
150
|
+
./scripts/redis-query.js -- HSCAN user:42 0 COUNT 100 # batched iteration (recommended)
|
|
151
|
+
# For small keys, HGETALL works (needs --allow-unbounded)
|
|
152
|
+
./scripts/redis-query.js --allow-unbounded -- HGETALL user:42
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### List
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
./scripts/redis-query.js -- TYPE task:queue # confirm it's a list
|
|
159
|
+
./scripts/redis-query.js -- LLEN task:queue # length
|
|
160
|
+
./scripts/redis-query.js -- LRANGE task:queue 0 9 # first 10 (non-negative indexes, range ≤ max-items)
|
|
161
|
+
# For the tail: LLEN first, then compute a non-negative index; negative-index LRANGE ranges are unpredictable and need --allow-unbounded
|
|
162
|
+
./scripts/redis-query.js -- LLEN task:queue
|
|
163
|
+
./scripts/redis-query.js --allow-unbounded -- LRANGE task:queue -10 -1 # explicitly allowed negative range
|
|
164
|
+
./scripts/redis-query.js -- LINDEX task:queue -1 # last element (single value; negative index unrestricted)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Set
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
./scripts/redis-query.js -- TYPE online:users # confirm it's a set
|
|
171
|
+
./scripts/redis-query.js -- SCARD online:users # member count
|
|
172
|
+
./scripts/redis-query.js -- SISMEMBER online:users user:42 # membership check
|
|
173
|
+
./scripts/redis-query.js -- SSCAN online:users 0 COUNT 100 # batched iteration
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### ZSet
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
./scripts/redis-query.js -- TYPE leaderboard # confirm it's a zset
|
|
180
|
+
./scripts/redis-query.js -- ZCARD leaderboard # member count
|
|
181
|
+
./scripts/redis-query.js -- ZSCORE leaderboard user:42 # score lookup
|
|
182
|
+
./scripts/redis-query.js -- ZRANK leaderboard user:42 # ascending rank
|
|
183
|
+
./scripts/redis-query.js -- ZRANGEBYSCORE leaderboard 100 999 WITHSCORES # score range
|
|
184
|
+
./scripts/redis-query.js -- ZSCAN leaderboard 0 COUNT 100 # batched iteration
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Unbounded command risks & SCAN alternatives
|
|
188
|
+
|
|
189
|
+
These commands can block Redis for a long time on big keys (single-threaded model) and **require `--allow-unbounded` by default**:
|
|
190
|
+
|
|
191
|
+
| Dangerous command | Reason | SCAN alternative |
|
|
192
|
+
|----------|------|-----------|
|
|
193
|
+
| `KEYS pattern` | O(N) over all keys | `SCAN 0 MATCH pattern COUNT 100` |
|
|
194
|
+
| `HGETALL key` | O(N) returns all fields | `HSCAN key 0 COUNT 100` |
|
|
195
|
+
| `HKEYS key` | O(N) returns all field names | `HSCAN key 0 COUNT 100` (ignore values) |
|
|
196
|
+
| `HVALS key` | O(N) returns all values | `HSCAN key 0 COUNT 100` (ignore field names) |
|
|
197
|
+
| `SMEMBERS key` | O(N) returns all members | `SSCAN key 0 COUNT 100` |
|
|
198
|
+
|
|
199
|
+
**Production principle**: prefer SCAN; use `--allow-unbounded` only when the key is known to be small.
|
|
200
|
+
|
|
201
|
+
## CONFIG GET safe-parameter whitelist
|
|
202
|
+
|
|
203
|
+
`CONFIG GET` only allows these parameters (no credential leaks, no service behavior impact):
|
|
204
|
+
|
|
205
|
+
| Parameter | Notes |
|
|
206
|
+
|------|------|
|
|
207
|
+
| `maxmemory` | Max memory limit |
|
|
208
|
+
| `maxmemory-policy` | Eviction policy |
|
|
209
|
+
| `timeout` | Client idle timeout |
|
|
210
|
+
| `tcp-keepalive` | TCP keepalive interval |
|
|
211
|
+
| `databases` | Number of databases |
|
|
212
|
+
|
|
213
|
+
Querying other parameters (e.g. `requirepass`, `masterauth`, `save`) is rejected to prevent leaking sensitive config.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<!-- [2026-09-05]-[translated to English (skill is now English-only)]-[content unchanged] -->
|
|
2
|
+
# Read-Only Security Model
|
|
3
|
+
|
|
4
|
+
Security core of this skill: **three layers of defense guarantee read-only; any write operation is intercepted before it reaches the database**.
|
|
5
|
+
Application-layer interception cannot replace database privileges, though — production must use a dedicated read-only account.
|
|
6
|
+
|
|
7
|
+
## Threat model & attack surface
|
|
8
|
+
|
|
9
|
+
### Who can trigger this skill
|
|
10
|
+
|
|
11
|
+
- A user asking (via conversation) to query the database → the agent invokes the script
|
|
12
|
+
- A user unintentionally or deliberately passing SQL/commands containing writes
|
|
13
|
+
|
|
14
|
+
### Attack surface
|
|
15
|
+
|
|
16
|
+
1. **SQL injection**: achieving writes via SQL syntax (nested comments, newlines inside strings, etc.)
|
|
17
|
+
2. **Dangerous functions**: calling `SLEEP()`, `BENCHMARK()`, `GET_LOCK()` etc. through a legal SELECT (DoS)
|
|
18
|
+
3. **Multi-statement injection**: concatenating write statements with semicolons
|
|
19
|
+
4. **Redis command injection**: executing write commands via arguments or variants
|
|
20
|
+
|
|
21
|
+
## MySQL three-layer defense
|
|
22
|
+
|
|
23
|
+
### Layer 1: SQL lexical scanner
|
|
24
|
+
|
|
25
|
+
Before connecting, the script lexically scans the raw SQL text.
|
|
26
|
+
|
|
27
|
+
**Allowed statement types:**
|
|
28
|
+
- `SELECT ...`
|
|
29
|
+
- `WITH ... SELECT ...` (CTE)
|
|
30
|
+
- `SHOW ...`
|
|
31
|
+
- `DESCRIBE` / `DESC ...`
|
|
32
|
+
- `EXPLAIN SELECT ...`
|
|
33
|
+
|
|
34
|
+
**Rejected writes:** INSERT / UPDATE / DELETE / REPLACE / DROP / ALTER / CREATE / TRUNCATE / RENAME / GRANT / REVOKE / SET / LOCK / UNLOCK / CALL / LOAD DATA etc.
|
|
35
|
+
|
|
36
|
+
**Rejected comment forms (all banned, including legal SQL inside comments):**
|
|
37
|
+
- `#` single-line comment
|
|
38
|
+
- `-- ` (double dash + space) single-line comment
|
|
39
|
+
- `/* */` multi-line comment
|
|
40
|
+
- `/*! */` MySQL version comment
|
|
41
|
+
- `/*+ */` optimizer hint
|
|
42
|
+
|
|
43
|
+
> **Why ban comments?** Comments can nest malicious SQL (e.g. `SELECT 1; /* */ DROP TABLE x;`) and hide version-specific syntax. A lexical scanner cannot safely parse comment contents, so they are rejected outright.
|
|
44
|
+
|
|
45
|
+
**Additional restrictions:**
|
|
46
|
+
- No backslash escapes (`\`) — prevents crafting legal-but-dangerous syntax via escapes
|
|
47
|
+
- No multi-statements (`;` separated) — prevents concatenated writes
|
|
48
|
+
- No `INTO OUTFILE` (exports to the server filesystem)
|
|
49
|
+
- No `FOR UPDATE` / `LOCK IN SHARE MODE` (explicit locking)
|
|
50
|
+
- No potential DoS functions like `SLEEP()` / `BENCHMARK()` / `GET_LOCK()`
|
|
51
|
+
- Auto-inject a top-level `LIMIT` when none is present (prevents long-blocking full scans)
|
|
52
|
+
|
|
53
|
+
> **Why a lexical scanner instead of regex?** Regex cannot reliably handle SQL string literals, identifier quoting (backticks), and operator ambiguity. A token-by-token scanner is more reliable than regex, yet it is still not a full SQL parser — complex but legal SQL may be rejected. That is a security trade-off.
|
|
54
|
+
|
|
55
|
+
### Layer 2: connection-level protection
|
|
56
|
+
|
|
57
|
+
Once connected:
|
|
58
|
+
- `multipleStatements: false`: the mysql2 driver explicitly disables multi-statement execution; even if the scanner misses a semicolon, subsequent statements never run
|
|
59
|
+
- `START TRANSACTION READ ONLY`: an explicit read-only transaction; the MySQL server itself rejects writes
|
|
60
|
+
- Always `ROLLBACK` after the query; never `COMMIT`
|
|
61
|
+
|
|
62
|
+
> **Why START TRANSACTION READ ONLY + ROLLBACK?** Defense in depth: even if the lexical scanner were bypassed (theoretically possible), the read-only transaction intercepts writes server-side, and ROLLBACK guarantees no side effects.
|
|
63
|
+
|
|
64
|
+
### Layer 3: account-level privileges
|
|
65
|
+
|
|
66
|
+
**Strongly recommended**: a dedicated read-only MySQL account granted only SELECT and SHOW VIEW.
|
|
67
|
+
This is the final boundary — even if both layers above fail, account privileges still block writes.
|
|
68
|
+
|
|
69
|
+
> ⚠️ **The admin commands below are for DBAs only; this skill's scripts never execute them:**
|
|
70
|
+
|
|
71
|
+
```sql
|
|
72
|
+
-- Read-only account setup example (for reference only)
|
|
73
|
+
CREATE USER 'db_query_reader'@'%' IDENTIFIED BY 'strong-password';
|
|
74
|
+
GRANT SELECT, SHOW VIEW ON `mydb`.* TO 'db_query_reader'@'%';
|
|
75
|
+
FLUSH PRIVILEGES;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Redis security model
|
|
79
|
+
|
|
80
|
+
### Whitelist design
|
|
81
|
+
|
|
82
|
+
Redis uses a **command whitelist**, not a blacklist.
|
|
83
|
+
|
|
84
|
+
**Why a whitelist?**
|
|
85
|
+
- A blacklist must enumerate every dangerous command, but Redis keeps adding new ones and Redis Modules can register custom commands. A whitelist rejects unknown commands by default (fail-closed) — safer.
|
|
86
|
+
- Blacklists are easily bypassed by new/module commands.
|
|
87
|
+
|
|
88
|
+
### Allowed commands (full list)
|
|
89
|
+
|
|
90
|
+
| Category | Commands | Notes |
|
|
91
|
+
|------|------|------|
|
|
92
|
+
| **General** | PING, GET, MGET, EXISTS, TYPE, TTL, PTTL, STRLEN, SCAN, KEYS†, DBSIZE, TIME | |
|
|
93
|
+
| **Hash** | HGET, HMGET, HEXISTS, HLEN, HGETALL†, HKEYS†, HVALS†, HSCAN | |
|
|
94
|
+
| **List** | LINDEX, LLEN, LRANGE | |
|
|
95
|
+
| **Set** | SISMEMBER, SCARD, SMEMBERS†, SSCAN | |
|
|
96
|
+
| **ZSet** | ZCARD, ZCOUNT, ZSCORE, ZRANK, ZREVRANK, ZRANGE, ZRANGEBYSCORE, ZSCAN | |
|
|
97
|
+
| **Server info** | INFO, CLIENT LIST, CONFIG GET† | CONFIG GET limited to safe items |
|
|
98
|
+
|
|
99
|
+
> † Requires `--allow-unbounded`. These can block Redis on big keys; prefer SCAN in production.
|
|
100
|
+
|
|
101
|
+
### Rejected commands
|
|
102
|
+
|
|
103
|
+
Everything not on the whitelist is rejected (fail-closed), including but not limited to:
|
|
104
|
+
- **Write commands**: SET, DEL, EXPIRE, INCR, DECR, HSET, LPUSH, SADD, ZADD, PFADD etc.
|
|
105
|
+
- **Dangerous commands**: FLUSHALL, FLUSHDB, SHUTDOWN, DEBUG, EVAL, EVALSHA, SCRIPT
|
|
106
|
+
- **Admin commands**: CONFIG SET, RENAME, MOVE, RESTORE, MIGRATE, SORT (may write to external storage)
|
|
107
|
+
- **Unrecognized commands**: including those registered by Redis Modules
|
|
108
|
+
|
|
109
|
+
### db is chosen via connection config
|
|
110
|
+
|
|
111
|
+
The Redis `db` number can only be set through the `--db` connection option; the **`SELECT` command is not exposed**.
|
|
112
|
+
This prevents switching databases mid-query and avoids touching the wrong db.
|
|
113
|
+
|
|
114
|
+
### Gate mechanism for KEYS and other big commands
|
|
115
|
+
|
|
116
|
+
`KEYS`, `HGETALL`, `HKEYS`, `HVALS`, `SMEMBERS` can block Redis for a long time on big keys (single-threaded model).
|
|
117
|
+
They are rejected by default and require an explicit `--allow-unbounded`, forcing awareness of the risk.
|
|
118
|
+
|
|
119
|
+
**Production alternative — SCAN-family commands:**
|
|
120
|
+
- `KEYS pattern` → `SCAN 0 MATCH pattern COUNT 100`
|
|
121
|
+
- `HGETALL key` → `HSCAN key 0 COUNT 100`
|
|
122
|
+
- `SMEMBERS key` → `SSCAN key 0 COUNT 100`
|
|
123
|
+
|
|
124
|
+
### CONFIG GET safe-parameter whitelist
|
|
125
|
+
|
|
126
|
+
`CONFIG GET` only allows these safe parameters (read-only config that doesn't affect service behavior):
|
|
127
|
+
- `maxmemory`
|
|
128
|
+
- `maxmemory-policy`
|
|
129
|
+
- `timeout`
|
|
130
|
+
- `tcp-keepalive`
|
|
131
|
+
- `databases`
|
|
132
|
+
|
|
133
|
+
Other config items (e.g. `requirepass`, `masterauth`) may leak credentials and are always rejected.
|
|
134
|
+
|
|
135
|
+
### Redis ACL as the final server-side boundary
|
|
136
|
+
|
|
137
|
+
**Strongly recommended**: a dedicated Redis ACL user for queries:
|
|
138
|
+
```
|
|
139
|
+
# Redis ACL example (for DBAs/ops only)
|
|
140
|
+
ACL SETUSER db_query_reader on >password ~* +@read +ping +info +client|list +config|get
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> ⚠️ A client-side timeout (`--timeout-ms`) only closes the client socket; it **does not cancel the command running server-side**. If a command takes too long on the server (e.g. `KEYS *` over a million keys), Redis keeps executing it after the client disconnects, blocking other requests meanwhile. This is inherent to Redis's single-threaded model, which is why the script gates dangerous commands client-side.
|
|
144
|
+
|
|
145
|
+
## Credentials & TLS conventions
|
|
146
|
+
|
|
147
|
+
### Password safety
|
|
148
|
+
|
|
149
|
+
- **No CLI `--password`**: command-line arguments land in shell history (`~/.bash_history` / `~/.zsh_history`) and may be readable by others.
|
|
150
|
+
- **Recommended**:
|
|
151
|
+
1. Environment variables (`DB_QUERY_MYSQL_PASSWORD` / `DB_QUERY_REDIS_PASSWORD`)
|
|
152
|
+
2. Env file (`--env-file=<path>`) with `0600` permissions
|
|
153
|
+
- Scripts never echo passwords in output.
|
|
154
|
+
|
|
155
|
+
### TLS
|
|
156
|
+
|
|
157
|
+
- Both MySQL and Redis support `--tls` for encrypted connections.
|
|
158
|
+
- `--ca-file` specifies a CA certificate for server identity verification.
|
|
159
|
+
- **Enable TLS** for cross-network connections in production.
|
|
160
|
+
|
|
161
|
+
## Failure mode: fail-closed
|
|
162
|
+
|
|
163
|
+
All safety checks follow the **fail-closed** principle:
|
|
164
|
+
- Unknown token in the SQL lexical scan → reject
|
|
165
|
+
- Unknown Redis command → reject
|
|
166
|
+
- Timeout → disconnect, never continue
|
|
167
|
+
- No "lenient mode" or fallback — a rejection is a rejection
|
|
168
|
+
|
|
169
|
+
## Known limitations
|
|
170
|
+
|
|
171
|
+
| Limitation | Impact | Mitigation |
|
|
172
|
+
|------|------|------|
|
|
173
|
+
| Lexer is not a full SQL parser | Complex but legal SQL (comments, backslash escapes, dynamic LIMIT, EXPLAIN ANALYZE) is rejected | Security trade-off; no lenient fallback added |
|
|
174
|
+
| MySQL `MAX_EXECUTION_TIME` only on 5.7.8+/8.0 | Query timeout not guaranteed on MariaDB | Dedicated read-only account + connection-level timeout as backstop |
|
|
175
|
+
| Redis client timeout doesn't cancel server-side commands | Big-key commands can keep blocking Redis | Gate dangerous commands client-side; restrict via ACL server-side |
|
|
176
|
+
| Application-layer checks can't replace DB privileges | A script bug could bypass client-side checks | Dedicated read-only account + ACL mandatory in production/shared environments |
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 严格 CLI 参数解析器
|
|
5
|
+
* 支持 --key=value、--key value、布尔 flag、-- 分隔 SQL/Redis 命令
|
|
6
|
+
* 未知选项、重复冲突、越界数字 → 立即失败(fail-closed)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class CliError extends Error {
|
|
10
|
+
constructor(msg) { super(msg); this.name = 'CliError'; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 解析 CLI 参数,返回 { options: {}, rest: string[] }
|
|
15
|
+
* @param {string[]} argv - process.argv.slice(2)
|
|
16
|
+
* @param {object} spec - 参数规格 { name: { type, default, min, max, alias } }
|
|
17
|
+
*/
|
|
18
|
+
function parseCli(argv, spec) {
|
|
19
|
+
const options = {};
|
|
20
|
+
const rest = [];
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
|
|
23
|
+
// 填充默认值
|
|
24
|
+
for (const [key, def] of Object.entries(spec)) {
|
|
25
|
+
if (def.default !== undefined) options[key] = def.default;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (i < argv.length) {
|
|
30
|
+
const arg = argv[i];
|
|
31
|
+
|
|
32
|
+
// -- 分隔符:后续全部作为位置参数
|
|
33
|
+
if (arg === '--') {
|
|
34
|
+
rest.push(...argv.slice(i + 1));
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --option=value 或 --option value 形式
|
|
39
|
+
if (arg.startsWith('--')) {
|
|
40
|
+
let key, value;
|
|
41
|
+
const eqIdx = arg.indexOf('=');
|
|
42
|
+
if (eqIdx !== -1) {
|
|
43
|
+
key = arg.slice(2, eqIdx);
|
|
44
|
+
value = arg.slice(eqIdx + 1);
|
|
45
|
+
} else {
|
|
46
|
+
key = arg.slice(2);
|
|
47
|
+
// 查找规格中该选项的类型
|
|
48
|
+
const s = findSpec(spec, key);
|
|
49
|
+
if (!s) throw new CliError(`未知选项: --${key}`);
|
|
50
|
+
if (s.type === 'boolean') {
|
|
51
|
+
value = true;
|
|
52
|
+
} else {
|
|
53
|
+
i++;
|
|
54
|
+
if (i >= argv.length) throw new CliError(`--${key} 需要值`);
|
|
55
|
+
value = argv[i];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resolved = resolveAlias(spec, key);
|
|
60
|
+
if (!resolved) throw new CliError(`未知选项: --${key}`);
|
|
61
|
+
|
|
62
|
+
// 重复选项检查
|
|
63
|
+
if (seen.has(resolved)) throw new CliError(`重复选项: --${resolved}`);
|
|
64
|
+
seen.add(resolved);
|
|
65
|
+
|
|
66
|
+
const s = spec[resolved];
|
|
67
|
+
options[resolved] = coerceValue(resolved, value, s);
|
|
68
|
+
} else {
|
|
69
|
+
// 非选项参数 → 位置参数
|
|
70
|
+
rest.push(arg);
|
|
71
|
+
}
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { options, rest };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 在 spec 或 alias 中查找选项名 */
|
|
79
|
+
function findSpec(spec, key) {
|
|
80
|
+
if (spec[key]) return spec[key];
|
|
81
|
+
for (const s of Object.values(spec)) {
|
|
82
|
+
if (s.alias === key) return s;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 解析别名到规范名 */
|
|
88
|
+
function resolveAlias(spec, key) {
|
|
89
|
+
if (spec[key]) return key;
|
|
90
|
+
for (const [name, s] of Object.entries(spec)) {
|
|
91
|
+
if (s.alias === key) return name;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 类型转换 + 边界检查 */
|
|
97
|
+
function coerceValue(key, raw, spec) {
|
|
98
|
+
if (spec.type === 'boolean') {
|
|
99
|
+
if (raw === 'true' || raw === true) return true;
|
|
100
|
+
if (raw === 'false' || raw === false) return false;
|
|
101
|
+
throw new CliError(`--${key} 期望布尔值,收到: ${raw}`);
|
|
102
|
+
}
|
|
103
|
+
if (spec.type === 'integer') {
|
|
104
|
+
const n = Number(raw);
|
|
105
|
+
if (!Number.isInteger(n)) throw new CliError(`--${key} 期望整数,收到: ${raw}`);
|
|
106
|
+
if (spec.min !== undefined && n < spec.min) throw new CliError(`--${key} 最小值 ${spec.min},收到: ${n}`);
|
|
107
|
+
if (spec.max !== undefined && n > spec.max) throw new CliError(`--${key} 最大值 ${spec.max},收到: ${n}`);
|
|
108
|
+
return n;
|
|
109
|
+
}
|
|
110
|
+
if (spec.type === 'string' && spec.choices) {
|
|
111
|
+
const lower = String(raw).toLowerCase();
|
|
112
|
+
if (!spec.choices.includes(lower)) throw new CliError(`--${key} 必须为 ${spec.choices.join('|')} 之一,收到: ${raw}`);
|
|
113
|
+
return lower;
|
|
114
|
+
}
|
|
115
|
+
return String(raw);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { parseCli, CliError };
|