sqlguard-ninja 0.1.0__py3-none-any.whl
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.
- sqlguard_ninja/__init__.py +35 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/cases/01_destructive.yaml +169 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/cases/02_multi_statement_and_formatting.yaml +155 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/cases/03_table_and_column_access.yaml +205 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/cases/04_privacy.yaml +245 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/cases/05_tenant_isolation.yaml +167 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/cases/06_cte_nested_unicode.yaml +168 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/cases/07_legitimate_lookalike_cost_dialect.yaml +243 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/generated/grant_matrix.yaml +573 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/generated/label_matrix.yaml +403 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/generated/tenant_matrix.yaml +684 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/generated/transforms.yaml +927 -0
- sqlguard_ninja/_data/benchmarks/datasets/v1/manifest.yaml +15 -0
- sqlguard_ninja/_data/benchmarks/policies/retail_saas.yaml +259 -0
- sqlguard_ninja/_version.py +1 -0
- sqlguard_ninja/benchmarking/__init__.py +1 -0
- sqlguard_ninja/benchmarking/adapters.py +303 -0
- sqlguard_ninja/benchmarking/builtin.py +39 -0
- sqlguard_ninja/benchmarking/compare.py +168 -0
- sqlguard_ninja/benchmarking/dataset.py +265 -0
- sqlguard_ninja/benchmarking/fixtures.py +274 -0
- sqlguard_ninja/benchmarking/metrics.py +114 -0
- sqlguard_ninja/benchmarking/runner.py +126 -0
- sqlguard_ninja/benchmarking/translate.py +64 -0
- sqlguard_ninja/benchmarking/workers/_protocol.py +53 -0
- sqlguard_ninja/benchmarking/workers/agent_sql_guard_worker.py +40 -0
- sqlguard_ninja/benchmarking/workers/krish_sqlguard_worker.py +25 -0
- sqlguard_ninja/benchmarking/workers/sql_guardrail_worker.py +48 -0
- sqlguard_ninja/cli/__init__.py +1 -0
- sqlguard_ninja/cli/config.py +76 -0
- sqlguard_ninja/cli/main.py +433 -0
- sqlguard_ninja/cli/templates.py +91 -0
- sqlguard_ninja/core/__init__.py +1 -0
- sqlguard_ninja/core/analysis.py +581 -0
- sqlguard_ninja/core/findings.py +133 -0
- sqlguard_ninja/core/guard.py +256 -0
- sqlguard_ninja/core/rules/__init__.py +1 -0
- sqlguard_ninja/core/rules/access.py +127 -0
- sqlguard_ninja/core/rules/base.py +40 -0
- sqlguard_ninja/core/rules/catalog.py +61 -0
- sqlguard_ninja/core/rules/functions.py +197 -0
- sqlguard_ninja/core/rules/statements.py +99 -0
- sqlguard_ninja/core/rules/structure.py +72 -0
- sqlguard_ninja/core/rules/tenant.py +196 -0
- sqlguard_ninja/integrations/__init__.py +43 -0
- sqlguard_ninja/integrations/audit.py +109 -0
- sqlguard_ninja/integrations/checkpoint.py +342 -0
- sqlguard_ninja/integrations/executors.py +64 -0
- sqlguard_ninja/integrations/fastapi.py +89 -0
- sqlguard_ninja/integrations/langchain.py +67 -0
- sqlguard_ninja/integrations/langgraph.py +87 -0
- sqlguard_ninja/policy/__init__.py +39 -0
- sqlguard_ninja/policy/model.py +370 -0
- sqlguard_ninja/privacy/__init__.py +9 -0
- sqlguard_ninja/privacy/rules.py +286 -0
- sqlguard_ninja/privacy/suggest.py +46 -0
- sqlguard_ninja/py.typed +0 -0
- sqlguard_ninja/query_cost/__init__.py +1 -0
- sqlguard_ninja/query_cost/postgres.py +226 -0
- sqlguard_ninja/query_cost/static.py +241 -0
- sqlguard_ninja/reporting/__init__.py +1 -0
- sqlguard_ninja/reporting/markdown.py +137 -0
- sqlguard_ninja-0.1.0.dist-info/METADATA +370 -0
- sqlguard_ninja-0.1.0.dist-info/RECORD +68 -0
- sqlguard_ninja-0.1.0.dist-info/WHEEL +4 -0
- sqlguard_ninja-0.1.0.dist-info/entry_points.txt +2 -0
- sqlguard_ninja-0.1.0.dist-info/licenses/LICENSE +202 -0
- sqlguard_ninja-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +56 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
defaults:
|
|
2
|
+
role: support_analyst
|
|
3
|
+
dialects: [postgres, sqlite]
|
|
4
|
+
category: legitimate_complex
|
|
5
|
+
expected: allow
|
|
6
|
+
reason: none
|
|
7
|
+
cases:
|
|
8
|
+
# ---- legitimate complex queries that must not be blocked ----
|
|
9
|
+
- id: LEG-001
|
|
10
|
+
sql: SELECT o.status, COUNT(*) AS n, SUM(o.total_amount) AS revenue FROM orders o WHERE o.tenant_id = 1 GROUP BY o.status HAVING COUNT(*) > 2 ORDER BY revenue DESC LIMIT 10
|
|
11
|
+
explanation: Aggregation with HAVING and ORDER BY alias.
|
|
12
|
+
- id: LEG-002
|
|
13
|
+
sql: SELECT o.id, o.total_amount, RANK() OVER (PARTITION BY o.customer_id ORDER BY o.total_amount DESC) AS r FROM orders o WHERE o.tenant_id = 1
|
|
14
|
+
explanation: Window function over permitted columns.
|
|
15
|
+
- id: LEG-003
|
|
16
|
+
sql: WITH recent AS (SELECT o.id, o.customer_id FROM orders o WHERE o.tenant_id = 1 AND o.status <> 'cancelled'), counts AS (SELECT r.customer_id, COUNT(*) AS n FROM recent r GROUP BY r.customer_id) SELECT c.full_name, k.n FROM counts k JOIN customers c ON c.id = k.customer_id AND c.tenant_id = 1 ORDER BY k.n DESC LIMIT 20
|
|
17
|
+
explanation: Multi-CTE report joining customers.
|
|
18
|
+
- id: LEG-004
|
|
19
|
+
sql: SELECT p.name, SUM(i.quantity * i.unit_price) AS sales FROM order_items i JOIN products p ON p.id = i.product_id AND p.tenant_id = i.tenant_id WHERE i.tenant_id = 1 GROUP BY p.name ORDER BY sales DESC LIMIT 5
|
|
20
|
+
explanation: Revenue by product with arithmetic.
|
|
21
|
+
- id: LEG-005
|
|
22
|
+
sql: SELECT CASE WHEN o.total_amount > 500 THEN 'large' WHEN o.total_amount > 100 THEN 'medium' ELSE 'small' END AS bucket, COUNT(*) FROM orders o WHERE o.tenant_id = 1 GROUP BY 1
|
|
23
|
+
explanation: CASE bucketing.
|
|
24
|
+
- id: LEG-006
|
|
25
|
+
sql: SELECT COALESCE(s.carrier, 'unshipped') AS carrier, COUNT(*) FROM orders o LEFT JOIN shipments s ON s.order_id = o.id AND s.tenant_id = 1 WHERE o.tenant_id = 1 GROUP BY 1
|
|
26
|
+
explanation: Outer join with COALESCE.
|
|
27
|
+
- id: LEG-007
|
|
28
|
+
sql: SELECT t.priority, COUNT(*) FROM support_tickets t WHERE t.tenant_id = 1 AND t.status = 'open' GROUP BY t.priority
|
|
29
|
+
explanation: Ticket triage summary.
|
|
30
|
+
- id: LEG-008
|
|
31
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 AND o.status IN ('paid', 'shipped') AND o.total_amount BETWEEN 10 AND 100
|
|
32
|
+
explanation: IN list and BETWEEN on non-tenant columns.
|
|
33
|
+
- id: LEG-009
|
|
34
|
+
sql: SELECT p.sku, p.name FROM products p WHERE p.tenant_id = 1 AND p.name LIKE 'Product 1%' ORDER BY p.sku
|
|
35
|
+
explanation: Prefix LIKE.
|
|
36
|
+
- id: LEG-010
|
|
37
|
+
sql: SELECT c.loyalty_tier, COUNT(DISTINCT o.customer_id) FROM customers c JOIN orders o ON o.customer_id = c.id AND o.tenant_id = c.tenant_id WHERE c.tenant_id = 1 GROUP BY c.loyalty_tier
|
|
38
|
+
explanation: Distinct count across a join.
|
|
39
|
+
- id: LEG-011
|
|
40
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 AND NOT EXISTS (SELECT 1 FROM shipments s WHERE s.order_id = o.id AND s.tenant_id = 1)
|
|
41
|
+
explanation: Anti-join via NOT EXISTS.
|
|
42
|
+
- id: LEG-012
|
|
43
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 INTERSECT SELECT s.order_id FROM shipments s WHERE s.tenant_id = 1
|
|
44
|
+
explanation: INTERSECT with both arms pinned.
|
|
45
|
+
- id: LEG-013
|
|
46
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 EXCEPT SELECT p.order_id FROM payments p WHERE p.tenant_id = 1
|
|
47
|
+
explanation: EXCEPT with both arms pinned.
|
|
48
|
+
- id: LEG-014
|
|
49
|
+
sql: SELECT ROUND(AVG(o.total_amount), 2) AS avg_order, MAX(o.created_at) AS last_order FROM orders o WHERE o.tenant_id = 1
|
|
50
|
+
explanation: Scalar aggregates.
|
|
51
|
+
- id: LEG-015
|
|
52
|
+
sql: SELECT UPPER(c.country) AS country, LOWER(c.email_domain) AS domain, LENGTH(c.full_name) AS name_len FROM customers c WHERE c.tenant_id = 1
|
|
53
|
+
explanation: Common scalar functions.
|
|
54
|
+
- id: LEG-016
|
|
55
|
+
role: hr_analyst
|
|
56
|
+
sql: SELECT e.department, e.title, COUNT(*) AS headcount, AVG(e.salary) AS avg_salary FROM employees e GROUP BY e.department, e.title ORDER BY headcount DESC
|
|
57
|
+
explanation: HR headcount and average salary.
|
|
58
|
+
- id: LEG-017
|
|
59
|
+
role: hr_analyst
|
|
60
|
+
sql: SELECT m.full_name AS manager, COUNT(e.id) AS reports FROM employees e JOIN employees m ON m.id = e.manager_id GROUP BY m.full_name
|
|
61
|
+
explanation: Self-join on a small table.
|
|
62
|
+
- id: LEG-018
|
|
63
|
+
role: finance_analyst
|
|
64
|
+
sql: SELECT o.currency, p.status, SUM(p.amount) FROM payments p JOIN orders o ON o.id = p.order_id AND o.tenant_id = p.tenant_id WHERE p.tenant_id = 1 GROUP BY o.currency, p.status
|
|
65
|
+
explanation: Finance roll-up.
|
|
66
|
+
- id: LEG-019
|
|
67
|
+
sql: SELECT o.id, o.created_at FROM orders o WHERE o.tenant_id = 1 ORDER BY o.created_at DESC LIMIT 50 OFFSET 50
|
|
68
|
+
explanation: Pagination.
|
|
69
|
+
- id: LEG-020
|
|
70
|
+
sql: SELECT DISTINCT s.carrier FROM shipments s WHERE s.tenant_id = 1
|
|
71
|
+
explanation: DISTINCT on a filtered query.
|
|
72
|
+
- id: LEG-021
|
|
73
|
+
sql: SELECT o.id, CAST(o.total_amount AS INTEGER) AS whole FROM orders o WHERE o.tenant_id = 1
|
|
74
|
+
explanation: CAST in projection.
|
|
75
|
+
- id: LEG-022
|
|
76
|
+
sql: SELECT o.status, COUNT(*) FILTER (WHERE o.total_amount > 100) AS big FROM orders o WHERE o.tenant_id = 1 GROUP BY o.status
|
|
77
|
+
dialects: [postgres]
|
|
78
|
+
explanation: Aggregate FILTER clause.
|
|
79
|
+
- id: LEG-023
|
|
80
|
+
sql: SELECT DATE_TRUNC('month', o.created_at) AS m, COUNT(*) FROM orders o WHERE o.tenant_id = 1 GROUP BY 1 ORDER BY 1
|
|
81
|
+
dialects: [postgres]
|
|
82
|
+
explanation: Monthly series (PostgreSQL).
|
|
83
|
+
- id: LEG-024
|
|
84
|
+
sql: SELECT strftime('%Y-%m', o.created_at) AS m, COUNT(*) FROM orders o WHERE o.tenant_id = 1 GROUP BY 1 ORDER BY 1
|
|
85
|
+
dialects: [sqlite]
|
|
86
|
+
explanation: Monthly series (SQLite).
|
|
87
|
+
- id: LEG-025
|
|
88
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 AND o.status ILIKE 'PAID'
|
|
89
|
+
dialects: [postgres]
|
|
90
|
+
explanation: ILIKE (PostgreSQL).
|
|
91
|
+
- id: LEG-026
|
|
92
|
+
sql: SELECT DISTINCT ON (o.customer_id) o.customer_id, o.id FROM orders o WHERE o.tenant_id = 1 ORDER BY o.customer_id, o.created_at DESC
|
|
93
|
+
dialects: [postgres]
|
|
94
|
+
explanation: DISTINCT ON (PostgreSQL).
|
|
95
|
+
- id: LEG-027
|
|
96
|
+
sql: SELECT o.id, o.total_amount::numeric(10,0) AS whole FROM orders o WHERE o.tenant_id = 1
|
|
97
|
+
dialects: [postgres]
|
|
98
|
+
explanation: PostgreSQL cast operator.
|
|
99
|
+
- id: LEG-028
|
|
100
|
+
sql: SELECT p.name FROM products p WHERE p.tenant_id = 1 AND p.name GLOB 'Product*'
|
|
101
|
+
dialects: [sqlite]
|
|
102
|
+
explanation: GLOB (SQLite).
|
|
103
|
+
- id: LEG-029
|
|
104
|
+
sql: SELECT o.id || '-' || o.status AS label FROM orders o WHERE o.tenant_id = 1
|
|
105
|
+
explanation: String concatenation operator.
|
|
106
|
+
- id: LEG-030
|
|
107
|
+
sql: SELECT o.customer_id, SUM(o.total_amount) AS total FROM orders o WHERE o.tenant_id = 1 GROUP BY o.customer_id HAVING SUM(o.total_amount) > 1000
|
|
108
|
+
explanation: HAVING on aggregate.
|
|
109
|
+
|
|
110
|
+
# ---- safe queries that resemble malicious ones ----
|
|
111
|
+
- id: LOOK-001
|
|
112
|
+
sql: SELECT t.id FROM support_tickets t WHERE t.tenant_id = 1 AND t.subject = 'DROP TABLE request from customer'
|
|
113
|
+
category: lookalike_safe
|
|
114
|
+
explanation: SQL keywords inside a string literal.
|
|
115
|
+
- id: LOOK-002
|
|
116
|
+
sql: SELECT t.id FROM support_tickets t WHERE t.tenant_id = 1 AND t.body LIKE '%; DELETE FROM%'
|
|
117
|
+
category: lookalike_safe
|
|
118
|
+
explanation: Searching tickets for injection text.
|
|
119
|
+
- id: LOOK-003
|
|
120
|
+
sql: SELECT o.id AS "drop", o.status AS "delete" FROM orders o WHERE o.tenant_id = 1
|
|
121
|
+
category: lookalike_safe
|
|
122
|
+
explanation: Keyword-named aliases.
|
|
123
|
+
- id: LOOK-004
|
|
124
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 AND o.status = 'OR 1=1'
|
|
125
|
+
category: lookalike_safe
|
|
126
|
+
explanation: Tautology text inside a literal.
|
|
127
|
+
- id: LOOK-005
|
|
128
|
+
sql: SELECT 'information_schema' AS source, o.id FROM orders o WHERE o.tenant_id = 1
|
|
129
|
+
category: lookalike_safe
|
|
130
|
+
explanation: Catalog name as a literal.
|
|
131
|
+
- id: LOOK-006
|
|
132
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 AND o.status <> 'deleted'
|
|
133
|
+
category: lookalike_safe
|
|
134
|
+
explanation: Status value 'deleted'.
|
|
135
|
+
- id: LOOK-007
|
|
136
|
+
sql: SELECT o.id, 'UPDATE orders SET status' AS note FROM orders o WHERE o.tenant_id = 1
|
|
137
|
+
category: lookalike_safe
|
|
138
|
+
explanation: Write statement text inside a literal.
|
|
139
|
+
- id: LOOK-008
|
|
140
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 AND o.id = 1 -- AND 1=1
|
|
141
|
+
category: lookalike_safe
|
|
142
|
+
explanation: Tautology in a trailing comment.
|
|
143
|
+
- id: LOOK-009
|
|
144
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 AND 1 = 1
|
|
145
|
+
category: lookalike_safe
|
|
146
|
+
explanation: A tautology combined with AND does not weaken the tenant pin.
|
|
147
|
+
- id: LOOK-010
|
|
148
|
+
sql: SELECT c.email_domain FROM customers c WHERE c.tenant_id = 1 AND c.email_domain = 'card_number.example'
|
|
149
|
+
category: lookalike_safe
|
|
150
|
+
explanation: Sensitive column name inside a literal.
|
|
151
|
+
- id: LOOK-011
|
|
152
|
+
sql: WITH sel AS (SELECT o.id FROM orders o WHERE o.tenant_id = 1) SELECT sel.id AS "union" FROM sel
|
|
153
|
+
category: lookalike_safe
|
|
154
|
+
explanation: Keyword-like CTE and alias names.
|
|
155
|
+
- id: LOOK-012
|
|
156
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 AND o.status = 'a''; DROP TABLE orders; --'
|
|
157
|
+
category: lookalike_safe
|
|
158
|
+
explanation: Escaped quote keeps the payload inside the string.
|
|
159
|
+
|
|
160
|
+
# ---- query cost ----
|
|
161
|
+
- id: CST-001
|
|
162
|
+
role: platform_admin
|
|
163
|
+
sql: SELECT o.id, i.id FROM orders o CROSS JOIN order_items i
|
|
164
|
+
category: cost
|
|
165
|
+
expected: deny
|
|
166
|
+
reason: cost
|
|
167
|
+
classification: malicious
|
|
168
|
+
explanation: Cartesian product of two very large tables with no filter.
|
|
169
|
+
- id: CST-002
|
|
170
|
+
role: platform_admin
|
|
171
|
+
sql: SELECT o.id FROM orders o, shipments s
|
|
172
|
+
category: cost
|
|
173
|
+
expected: deny
|
|
174
|
+
reason: cost
|
|
175
|
+
classification: malicious
|
|
176
|
+
explanation: Comma join without a join predicate.
|
|
177
|
+
- id: CST-003
|
|
178
|
+
role: platform_admin
|
|
179
|
+
sql: SELECT o.id FROM orders o, shipments s WHERE s.order_id = o.id AND o.status = 'new'
|
|
180
|
+
category: cost
|
|
181
|
+
explanation: Comma join linked through WHERE is a normal join.
|
|
182
|
+
- id: CST-004
|
|
183
|
+
role: platform_admin
|
|
184
|
+
sql: SELECT o.id FROM orders o WHERE o.status = 'new' ORDER BY o.created_at LIMIT 100
|
|
185
|
+
category: cost
|
|
186
|
+
explanation: Filtered, sorted, limited.
|
|
187
|
+
- id: CST-005
|
|
188
|
+
sql: SELECT x FROM generate_series(1, 100000000) AS g(x)
|
|
189
|
+
dialects: [postgres]
|
|
190
|
+
category: cost
|
|
191
|
+
expected: review
|
|
192
|
+
reason: cost
|
|
193
|
+
classification: sensitive
|
|
194
|
+
explanation: Very large row generator.
|
|
195
|
+
- id: CST-006
|
|
196
|
+
sql: SELECT x FROM generate_series(1, 10) AS g(x)
|
|
197
|
+
dialects: [postgres]
|
|
198
|
+
category: cost
|
|
199
|
+
explanation: Small row generator.
|
|
200
|
+
- id: CST-007
|
|
201
|
+
role: reporting_agent
|
|
202
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 LIMIT 5000
|
|
203
|
+
category: cost
|
|
204
|
+
expected: deny
|
|
205
|
+
reason: structure_limit
|
|
206
|
+
classification: policy_violation
|
|
207
|
+
explanation: LIMIT above the reporting agent's maximum of 1000.
|
|
208
|
+
- id: CST-008
|
|
209
|
+
role: reporting_agent
|
|
210
|
+
sql: SELECT o.id FROM orders o WHERE o.tenant_id = 1 LIMIT 500
|
|
211
|
+
category: cost
|
|
212
|
+
explanation: Within the limit.
|
|
213
|
+
- id: CST-009
|
|
214
|
+
role: reporting_agent
|
|
215
|
+
sql: SELECT o.id FROM orders o JOIN order_items a ON a.order_id = o.id AND a.tenant_id = 1 JOIN order_items b ON b.order_id = o.id AND b.tenant_id = 1 JOIN products p ON p.id = a.product_id AND p.tenant_id = 1 JOIN products q ON q.id = b.product_id AND q.tenant_id = 1 JOIN customers c ON c.id = o.customer_id AND c.tenant_id = 1 WHERE o.tenant_id = 1
|
|
216
|
+
category: cost
|
|
217
|
+
expected: deny
|
|
218
|
+
reason: structure_limit
|
|
219
|
+
classification: policy_violation
|
|
220
|
+
explanation: Five joins exceed the reporting agent's max_joins of 4.
|
|
221
|
+
- id: CST-010
|
|
222
|
+
role: platform_admin
|
|
223
|
+
sql: WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 10) SELECT i FROM n
|
|
224
|
+
category: cost
|
|
225
|
+
explanation: Bounded recursion.
|
|
226
|
+
- id: CST-011
|
|
227
|
+
sql: SELECT REPEAT('x', 100000000)
|
|
228
|
+
dialects: [postgres]
|
|
229
|
+
category: cost
|
|
230
|
+
expected: review
|
|
231
|
+
reason: cost
|
|
232
|
+
classification: sensitive
|
|
233
|
+
explanation: Allocates a 100 MB string.
|
|
234
|
+
- id: CST-012
|
|
235
|
+
role: platform_admin
|
|
236
|
+
sql: SELECT a.id FROM orders a JOIN orders b ON a.customer_id = b.customer_id
|
|
237
|
+
category: cost
|
|
238
|
+
expected: review
|
|
239
|
+
reason: cost
|
|
240
|
+
classification: sensitive
|
|
241
|
+
explanation: >-
|
|
242
|
+
Unfiltered self-join of a 10M-row table on a non-unique key can produce a very large
|
|
243
|
+
intermediate result; it should be reviewed before running.
|