Skip to content

Commit dd1fe2d

Browse files
committed
feat(runtime): add strict secrets mode to secrets provider
test(runtime): add strict-mode tests for file provider docs: clarify async LLM in README and architecture; add TDS parser support matrix; document SECRET_PROVIDER_MODE; TDS scope/risk in ENFORCEMENT chore(lint): fix E402/E702 and run fmt; translate Swedish comments in workflow
1 parent 8de9255 commit dd1fe2d

26 files changed

Lines changed: 214 additions & 102 deletions

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ LLM_ENDPOINT=http://localhost:11434
2828
LLM_SEND_EXTERNAL=false
2929
REDACTION_ENABLED=true
3030

31+
# --- Secrets provider ---
32+
# Choose provider: env|file
33+
SECRET_PROVIDER=env
34+
# Behavior: permissive (fallback to env) | strict (error if file missing)
35+
SECRET_PROVIDER_MODE=permissive
36+
3137
# --- Testing/integration toggles ---
3238
# When set, integration tests may connect to a live SQL Server
3339
ENABLE_INTEGRATION_TESTS=false

.github/workflows/publish-wiki.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,13 @@ jobs:
2222
cp docs/*.md wiki_temp/
2323
cd wiki_temp
2424
25-
# Byt namn på index.md till Home.md för att skapa startsidan
25+
# Rename index.md to Home.md to create the Wiki landing page
2626
if [ -f "index.md" ]; then
2727
mv index.md Home.md
2828
fi
2929
30-
# *** HÄR ÄR DEN SISTA FIXEN ***
31-
# Hitta alla .md-filer och ta bort ".md" från slutet av alla länkar inuti dem.
32-
# Detta gör att länkarna fungerar korrekt i GitHubs Wiki-system.
30+
# Final fix: remove trailing ".md" from internal links in all Markdown files
31+
# so links work correctly in GitHub Wiki.
3332
find . -type f -name "*.md" -exec sed -i 's/\.md)/)/g' {} +
3433
3534
git config user.name "github-actions[bot]"

README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
<br/>
1919
</p>
2020

21-
SQLumAI is an invisible, AI‑powered proxy for Microsoft SQL Server.
21+
SQLumAI is an invisible proxy for Microsoft SQL Server with out‑of‑band AI insights.
2222

2323
For non‑technical readers
2424
- What it does: Watches data flowing to SQL Server and helps improve data quality – without slowing anything down.
2525
- How it helps: Finds missing values, inconsistent formats (dates, phone numbers), and process gaps; proposes fixes and simpler input rules; summarizes issues daily.
2626
- Why it’s safe: It forwards traffic transparently by default (dry‑run). You control when to enforce rules.
27-
- Where AI fits: A local LLM turns raw events into a short list of high‑value actions and insights.
27+
- Where AI fits: A local LLM turns raw events into a short list of high‑value actions and insights (generated asynchronously, not inline).
2828

2929
### Dependencies
3030
- ![FastAPI](https://img.shields.io/pypi/v/fastapi?label=fastapi) ![Uvicorn](https://img.shields.io/pypi/v/uvicorn?label=uvicorn) ![Pydantic](https://img.shields.io/pypi/v/pydantic?label=pydantic) ![HTTPX](https://img.shields.io/pypi/v/httpx?label=httpx) ![prometheus_client](https://img.shields.io/pypi/v/prometheus_client?label=prometheus_client)
@@ -40,7 +40,7 @@ For non‑technical readers
4040
- MVP 3 – Gatekeeper: Rules API + engine, env‑gating, thresholds; optional TLS termination + TDS parsing for SQL Batch/RPC; simple column‑level autocorrect; metrics, audits, dashboards.
4141
- Dry‑run vs enforce: `ENFORCEMENT_MODE=log|enforce`
4242
- Parsers: `ENABLE_TDS_PARSER=true`, `ENABLE_SQL_TEXT_SNIFF=true`
43-
- LLM: `LLM_PROVIDER`, `LLM_MODEL`, `LLM_ENDPOINT` (Ollama default), `OPENAI_API_KEY` (OpenAI-compatible)
43+
- LLM: `LLM_PROVIDER`, `LLM_MODEL`, `LLM_ENDPOINT` (Ollama default), `OPENAI_API_KEY` (OpenAI-compatible). LLM runs async via scheduler jobs.
4444
- Scheduler: `ENABLE_SCHEDULER`, `SCHEDULE_INTERVAL_SEC`
4545
- Docs overview: see `docs/mvp.md`
4646

@@ -52,7 +52,7 @@ For non‑technical readers
5252
- NL→Rule suggestion: `POST /rules/suggest` returns a proposed rule JSON from plain text.
5353
- XEvents helper API: `POST /xevents/setup?mode=ring|file` returns a ready-to-run SQL session script.
5454
- XEvents setup helper: `scripts/setup_xevents.py` renders a session SQL for ring buffer or file targets.
55-
- Secrets provider: `src/runtime/secrets.py` with `SECRET_PROVIDER=env|file`.
55+
- Secrets provider: `src/runtime/secrets.py` with `SECRET_PROVIDER=env|file` and `SECRET_PROVIDER_MODE=permissive|strict`.
5656

5757
See `AGENTS.md` for contributor guidelines and development conventions.
5858

@@ -81,9 +81,18 @@ flowchart LR
8181
API --> P
8282
```
8383

84+
### Real‑time vs Async AI
85+
- Real‑time: The proxy applies deterministic rules to decide allow/autocorrect/block. No LLM calls occur on the hot path.
86+
- Async: Scheduler jobs read aggregated data and generate insights and proposed rules with an LLM.
87+
- Safety: Local‑first by default; no external LLM traffic unless explicitly configured.
88+
89+
### TDS Parser Support
90+
- Minimal, best‑effort parsing of Batch and RPC to keep the hot path safe.
91+
- See `docs/ENFORCEMENT.md` for current scope, limitations, and roadmap.
92+
8493
Docs
8594
- Browse docs in `docs/` or serve with `mkdocs serve`.
86-
- MVPs: `docs/mvp.md` | Enforcement: `docs/ENFORCEMENT.md` | Architecture: `docs/architecture.md` | HA: `docs/ha.md`
95+
- MVPs: `docs/mvp.md` | Enforcement: `docs/ENFORCEMENT.md` | TDS Support: `docs/tds-parser.md` | Architecture: `docs/architecture.md` | HA: `docs/ha.md`
8796
- LLM config/providers: `docs/llm-providers.md` | Insights: `docs/insights.md`
8897
- Reports/Integration: `docs/howto-reports.md`, `docs/howto-integration.md`
8998
- Metrics dashboard: `docs/metrics-dashboard.md`
@@ -177,7 +186,7 @@ curl -s -X POST http://localhost:8080/rules \
177186
# Suggest a rule from natural language (stub)
178187
curl -s -X POST http://localhost:8080/rules/suggest \
179188
-H 'Content-Type: application/json' \
180-
-d '{"text":"Alla svenska telefonnummer ska normaliseras"}' | jq .
189+
-d '{"text":"Normalize all Swedish phone numbers"}' | jq .
181190
```
182191

183192
## Usage Scenarios: BSS, Booking, ServiceNow, CRM
@@ -210,7 +219,7 @@ Principles:
210219
[
211220
{"id":"bss-phone-autocorrect","target":"column","selector":"dbo.Customers.Phone","action":"autocorrect","reason":"Normalize E.164","confidence":0.9},
212221
{"id":"bss-email-required","target":"column","selector":"dbo.Customers.Email","action":"block","reason":"Email required at onboarding","confidence":1.0},
213-
{"id":"bss-no-test-subs","target":"pattern","selector":"INSERT INTO dbo.Subscriptions","action":"block","reason":"Stoppa testabonnemang i prod","confidence":0.9}
222+
{"id":"bss-no-test-subs","target":"pattern","selector":"INSERT INTO dbo.Subscriptions","action":"block","reason":"Block test subscriptions in production","confidence":0.9}
214223
]
215224
```
216225
- LLM insights (examples):
@@ -225,7 +234,7 @@ Principles:
225234
```json
226235
[
227236
{"id":"booking-no-overlap","target":"pattern","selector":"INSERT INTO dbo.Bookings","action":"block","reason":"Overlapping times must be prevented in app logic","confidence":0.8},
228-
{"id":"booking-email-format","target":"column","selector":"dbo.Bookings.Email","action":"autocorrect","reason":"Korrigera vanliga typos","confidence":0.7}
237+
{"id":"booking-email-format","target":"column","selector":"dbo.Bookings.Email","action":"autocorrect","reason":"Correct common typos","confidence":0.7}
229238
]
230239
```
231240
- LLM insights (examples):

docs/ENFORCEMENT.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ This document explains how SQLumAI evolves from passive monitoring to selective,
1919
- Policy engine: For each candidate value, evaluate rules in order; emit decision (allow/autocorrect/block) + reason + confidence.
2020

2121
### SQL Batch (0x01)
22-
- Reassembly of full batch, decode as UTF‑16LE; apply pattern/table rules.
23-
- Column‑level mapping for simple INSERT/UPDATE via regex parser; safe rewrite of SQL text when `ENFORCEMENT_MODE=enforce`.
24-
- Multirow INSERT stöd: omskrivning av `(…), (…)` block när kolumn‑värde‑antal matchar.
22+
- Reassemble full batch, decode as UTF‑16LE; apply pattern/table rules.
23+
- Column‑level mapping for simple INSERT/UPDATE via a best‑effort regex parser; safe SQL text rewrite when `ENFORCEMENT_MODE=enforce`.
24+
- Multirow INSERT support: rewrite `(…), (…)` groups when the number of columns matches the number of values.
2525

2626
### RPC (0x03)
27-
- Reassembly av RPC payload; heuristisk extraktion av NVARCHAR‑parametrar.
28-
- In‑place autocorrect (utf‑16le) när nya värdet inte är längre än det gamla (kortare pad: spaces) när `RPC_AUTOCORRECT_INPLACE=true`.
29-
- Blockering av RPC vid regelmatchning när `ENFORCEMENT_MODE=enforce`.
27+
- Reassemble RPC payload; heuristic extraction of NVARCHAR parameters.
28+
- In‑place autocorrect (utf‑16le) when the new value is not longer than the original (shorter padded with spaces) when `RPC_AUTOCORRECT_INPLACE=true`.
29+
- Block RPC on rule match when `ENFORCEMENT_MODE=enforce`.
3030

3131
## Traceability & Safety
3232
- Logging: Every correction/block records rule id, reason, confidence, original and resulting value.
@@ -40,8 +40,14 @@ This document explains how SQLumAI evolves from passive monitoring to selective,
4040
4) Monitor: Track metrics (auto‑corrections, blocks, false positives) to tune thresholds.
4141

4242
## Feature flags / Env gating
43-
- Per‑regel: `enabled: true/false`, `apply_in_envs: ["dev","staging","prod"]` för att styra var regler gäller.
44-
- Globala toggles: `ENABLE_TDS_PARSER`, `ENABLE_SQL_TEXT_SNIFF`, `ENFORCEMENT_MODE`, `RPC_AUTOCORRECT_INPLACE`, `TIME_BUDGET_MS`, `MAX_REWRITE_BYTES`.
43+
- Per‑rule controls: `enabled: true/false`, `apply_in_envs: ["dev","staging","prod"]` to limit where rules apply.
44+
- Global toggles: `ENABLE_TDS_PARSER`, `ENABLE_SQL_TEXT_SNIFF`, `ENFORCEMENT_MODE`, `RPC_AUTOCORRECT_INPLACE`, `TIME_BUDGET_MS`, `MAX_REWRITE_BYTES`.
45+
46+
## TDS Parser Scope and Risk
47+
- SQL Server’s TDS protocol is complex. SQLumAI’s parsing is intentionally minimal and best‑effort to keep the hot path safe.
48+
- Supported today: UTF‑16LE batch text for simple INSERT/UPDATE matching and heuristic NVARCHAR RPC parameter extraction.
49+
- Not guaranteed: unusual datatypes, vendor‑specific RPCs, newer protocol nuances. On parse failure, the proxy fails open and logs context.
50+
- Roadmap: consider contributing to or integrating a robust external TDS library. Contributions welcome.
4551

4652
## Performance
4753
- Keep parsing minimal (batch/statement only); avoid heavy transforms on the hot path.

docs/architecture.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ flowchart LR
2323
XE --> R
2424
API --> P
2525
```
26+
27+
Notes
28+
- Real‑time decisions in the proxy are deterministic (rules engine). No LLM calls happen on the hot path.
29+
- LLM processing is asynchronous: scheduler jobs read aggregated data and generate insights and proposed rules.

docs/tds-parser.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# TDS Parser Support Matrix
2+
3+
This document outlines what the built‑in TDS handling currently supports, what is explicitly out of scope, and the safety posture. The goal is transparency so operators can decide when to enable parsing and enforcement.
4+
5+
## Philosophy
6+
- Keep the hot path safe and fast: minimal, best‑effort parsing only where it adds value.
7+
- Fail open: if parsing or mapping fails, the proxy forwards traffic unchanged and logs context.
8+
- Prefer explicit rules over heavy rewrites; keep autocorrects reversible and auditable.
9+
10+
## Summary Matrix
11+
12+
| Area | Support | Notes |
13+
|------|---------|-------|
14+
| TDS packet headers | Basic | Used for flow control and identifying packet types. |
15+
| SQL Batch (0x01) reassembly | Yes | UTF‑16LE decoding to recover batch text. |
16+
| SQL text analysis | Limited | Best‑effort regex for simple INSERT/UPDATE detection. |
17+
| Column mapping (INSERT) | Limited | Match column list to VALUES tuples when counts align. |
18+
| Multi‑row INSERT | Limited | Rewrites supported only when column/value counts match per tuple. |
19+
| Column mapping (UPDATE) | Limited | Heuristic mapping of SET column=value pairs (simple cases). |
20+
| MERGE/BULK/CTE/complex SQL | No | Not parsed beyond basic pattern checks; no rewrites. |
21+
| RPC (0x03) reassembly | Yes | Reconstruct payload for parameter extraction. |
22+
| RPC parameter types | Partial | Heuristic NVARCHAR extraction; other types not guaranteed. |
23+
| In‑place RPC autocorrect | Optional | When `RPC_AUTOCORRECT_INPLACE=true` and new value is not longer. |
24+
| TLS termination | Optional | Off by default; required to read payloads on the proxy. |
25+
26+
## Supported Scenarios (Examples)
27+
- Detect and optionally rewrite trivial `INSERT INTO dbo.Table (A,B) VALUES ("x","y")` when rules target specific columns.
28+
- Heuristically extract NVARCHAR RPC parameters for rule checks and lightweight autocorrect when safe in place.
29+
30+
## Not Covered / Limitations
31+
- Unusual or newer SQL Server datatypes (e.g., SQL_VARIANT, XML, TVP) are not guaranteed.
32+
- Vendor‑specific RPCs or non‑standard encodings.
33+
- Complex SQL constructs (MERGE, CTEs, nested queries) — analysis is pattern‑level only; no rewrites.
34+
35+
## Safety & Failure Modes
36+
- Fail‑open by default: undecided/failed parsing → forward unchanged and log.
37+
- Bounded rewrites: controlled by `TIME_BUDGET_MS` and `MAX_REWRITE_BYTES`.
38+
- Auditable: all corrections/blocks include rule id, reason, and confidence in logs/metrics.
39+
40+
## Configuration
41+
- Feature toggles: `ENABLE_TDS_PARSER`, `ENABLE_SQL_TEXT_SNIFF`, `ENFORCEMENT_MODE`, `RPC_AUTOCORRECT_INPLACE`, `TIME_BUDGET_MS`, `MAX_REWRITE_BYTES`.
42+
- TLS termination: `TLS_TERMINATION`, `TLS_CERT_PATH`, `TLS_KEY_PATH`.
43+
44+
## Tests & Coverage
45+
- Unit tests for batch/RPC parsing heuristics live under `tests/` (e.g., `test_tds_parser.py`, `test_rpc_parse.py`).
46+
- Integration tests are gated by environment variables and are optional (`ENABLE_INTEGRATION_TESTS`).
47+
48+
## Roadmap: External TDS Library
49+
To reduce protocol risk and broaden support, consider adopting or contributing to a robust TDS implementation. Evaluation criteria:
50+
- Protocol coverage (packets, datatypes, RPC formats, TLS).
51+
- Performance characteristics and memory footprint suitable for a proxy.
52+
- License compatibility (MIT‑friendly) and maintenance activity.
53+
- Extensibility hooks for safely mapping parameters to columns.
54+
55+
If you have experience with suitable libraries or are interested in collaborating, please open an issue or PR.
56+

scripts/replay_dryrun.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import datetime as dt
1111
from pathlib import Path
1212
from collections import defaultdict, Counter
13-
from src.policy.engine import PolicyEngine, Event, Rule
13+
from src.policy.engine import PolicyEngine, Event
1414
from src.policy.loader import load_rules
1515
from src.metrics import store as metrics_store
1616

scripts/validate_rules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import json
88
import sys
99
from pathlib import Path
10-
from typing import Any, Tuple
10+
from typing import Tuple
1111

1212

1313
def main():

src/api.py

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,54 @@
11
try:
22
from fastapi import FastAPI, HTTPException, Response
3-
except Exception: # minimal shim for environments without fastapi
4-
class HTTPException(Exception):
5-
def __init__(self, status_code: int = 500, detail: str = ""):
6-
super().__init__(detail)
7-
self.status_code = status_code
8-
9-
class Response: # type: ignore
10-
def __init__(self, content: str | bytes = b"", media_type: str = "text/html"):
11-
self.content = content
12-
self.media_type = media_type
13-
# For tests accessing body
14-
try:
15-
self.body = content if isinstance(content, bytes) else str(content).encode("utf-8")
16-
except Exception:
17-
self.body = b""
18-
19-
class DummyApp:
20-
def __init__(self, *_, **__):
21-
pass
22-
23-
def get(self, *_args, **_kwargs):
24-
def deco(fn):
25-
return fn
26-
return deco
27-
28-
def post(self, *_args, **_kwargs):
29-
def deco(fn):
30-
return fn
31-
return deco
32-
33-
def delete(self, *_args, **_kwargs):
34-
def deco(fn):
35-
return fn
36-
return deco
37-
38-
FastAPI = DummyApp # type: ignore
3+
except Exception: # minimal shim for environments without fastapi # pragma: no cover
4+
class HTTPException(Exception): # pragma: no cover
5+
def __init__(self, status_code: int = 500, detail: str = ""): # pragma: no cover
6+
super().__init__(detail) # pragma: no cover
7+
self.status_code = status_code # pragma: no cover
8+
9+
class Response: # type: ignore # pragma: no cover
10+
def __init__(self, content: str | bytes = b"", media_type: str = "text/html"): # pragma: no cover
11+
self.content = content # pragma: no cover
12+
self.media_type = media_type # pragma: no cover
13+
# For tests accessing body # pragma: no cover
14+
try: # pragma: no cover
15+
self.body = content if isinstance(content, bytes) else str(content).encode("utf-8") # pragma: no cover
16+
except Exception: # pragma: no cover
17+
self.body = b"" # pragma: no cover
18+
19+
class DummyApp: # pragma: no cover
20+
def __init__(self, *_, **__): # pragma: no cover
21+
pass # pragma: no cover
22+
23+
def get(self, *_args, **_kwargs): # pragma: no cover
24+
def deco(fn): # pragma: no cover
25+
return fn # pragma: no cover
26+
return deco # pragma: no cover
27+
28+
def post(self, *_args, **_kwargs): # pragma: no cover
29+
def deco(fn): # pragma: no cover
30+
return fn # pragma: no cover
31+
return deco # pragma: no cover
32+
33+
def delete(self, *_args, **_kwargs): # pragma: no cover
34+
def deco(fn): # pragma: no cover
35+
return fn # pragma: no cover
36+
return deco # pragma: no cover
37+
38+
FastAPI = DummyApp # type: ignore # pragma: no cover
3939
try:
4040
from pydantic import BaseModel, Field
41-
except Exception: # minimal shim for environments without pydantic
42-
class BaseModel: # type: ignore
43-
def __init__(self, **data):
44-
for k, v in data.items():
45-
setattr(self, k, v)
41+
except Exception: # minimal shim for environments without pydantic # pragma: no cover
42+
class BaseModel: # type: ignore # pragma: no cover
43+
def __init__(self, **data): # pragma: no cover
44+
for k, v in data.items(): # pragma: no cover
45+
setattr(self, k, v) # pragma: no cover
4646

47-
def model_dump(self):
48-
return {k: getattr(self, k) for k in self.__dict__.keys()}
47+
def model_dump(self): # pragma: no cover
48+
return {k: getattr(self, k) for k in self.__dict__.keys()} # pragma: no cover
4949

50-
def Field(default=None, **_): # type: ignore
51-
return default
50+
def Field(default=None, **_): # type: ignore # pragma: no cover
51+
return default # pragma: no cover
5252
from typing import List, Literal, Optional
5353
import json
5454
import os
@@ -59,8 +59,8 @@ def Field(default=None, **_): # type: ignore
5959
from scripts.setup_xevents import render_xevents_sql
6060
try:
6161
from src.version import __version__
62-
except Exception:
63-
__version__ = "0.0.0"
62+
except Exception: # pragma: no cover
63+
__version__ = "0.0.0" # pragma: no cover
6464

6565
app = FastAPI(title="SQLumAI Policy API", version=__version__)
6666

src/proxy/tds_proxy.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import os
44
import time
5+
import contextlib
56
from src.policy.loader import load_rules
67
from src.policy.engine import PolicyEngine, Event
78
from src.metrics import store as metrics_store
@@ -316,9 +317,6 @@ async def handle_client(local_reader: asyncio.StreamReader, local_writer: asynci
316317
logger.info(f"{conn_id} closed bytes c2s={counter.get('c2s',0)} s2c={counter.get('s2c',0)}")
317318

318319

319-
import contextlib
320-
321-
322320
async def run_proxy(listen_host: str, listen_port: int, upstream_host: str, upstream_port: int, stop_event: Optional[asyncio.Event] = None):
323321
server = await asyncio.start_server(
324322
lambda r, w: handle_client(r, w, upstream_host, upstream_port, f"conn-{id(w)}"), listen_host, listen_port

0 commit comments

Comments
 (0)