@connorbritain/mssql-mcp-reader 0.1.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/README.md +106 -0
- package/dist/audit/AuditLogger.d.ts +37 -0
- package/dist/audit/AuditLogger.d.ts.map +1 -0
- package/dist/audit/AuditLogger.js +145 -0
- package/dist/audit/AuditLogger.js.map +1 -0
- package/dist/config/EnvironmentManager.d.ts +70 -0
- package/dist/config/EnvironmentManager.d.ts.map +1 -0
- package/dist/config/EnvironmentManager.js +301 -0
- package/dist/config/EnvironmentManager.js.map +1 -0
- package/dist/config/ScriptManager.d.ts +69 -0
- package/dist/config/ScriptManager.d.ts.map +1 -0
- package/dist/config/ScriptManager.js +166 -0
- package/dist/config/ScriptManager.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +569 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/DescribeTableTool.d.ts +32 -0
- package/dist/tools/DescribeTableTool.d.ts.map +1 -0
- package/dist/tools/DescribeTableTool.js +108 -0
- package/dist/tools/DescribeTableTool.js.map +1 -0
- package/dist/tools/ExplainQueryTool.d.ts +24 -0
- package/dist/tools/ExplainQueryTool.d.ts.map +1 -0
- package/dist/tools/ExplainQueryTool.js +98 -0
- package/dist/tools/ExplainQueryTool.js.map +1 -0
- package/dist/tools/InspectDependenciesTool.d.ts +45 -0
- package/dist/tools/InspectDependenciesTool.d.ts.map +1 -0
- package/dist/tools/InspectDependenciesTool.js +215 -0
- package/dist/tools/InspectDependenciesTool.js.map +1 -0
- package/dist/tools/ListDatabasesTool.d.ts +27 -0
- package/dist/tools/ListDatabasesTool.d.ts.map +1 -0
- package/dist/tools/ListDatabasesTool.js +107 -0
- package/dist/tools/ListDatabasesTool.js.map +1 -0
- package/dist/tools/ListEnvironmentsTool.d.ts +49 -0
- package/dist/tools/ListEnvironmentsTool.d.ts.map +1 -0
- package/dist/tools/ListEnvironmentsTool.js +73 -0
- package/dist/tools/ListEnvironmentsTool.js.map +1 -0
- package/dist/tools/ListScriptsTool.d.ts +41 -0
- package/dist/tools/ListScriptsTool.d.ts.map +1 -0
- package/dist/tools/ListScriptsTool.js +86 -0
- package/dist/tools/ListScriptsTool.js.map +1 -0
- package/dist/tools/ListTableTool.d.ts +24 -0
- package/dist/tools/ListTableTool.d.ts.map +1 -0
- package/dist/tools/ListTableTool.js +85 -0
- package/dist/tools/ListTableTool.js.map +1 -0
- package/dist/tools/ProfileTableTool.d.ts +78 -0
- package/dist/tools/ProfileTableTool.d.ts.map +1 -0
- package/dist/tools/ProfileTableTool.js +372 -0
- package/dist/tools/ProfileTableTool.js.map +1 -0
- package/dist/tools/ReadDataTool.d.ts +61 -0
- package/dist/tools/ReadDataTool.d.ts.map +1 -0
- package/dist/tools/ReadDataTool.js +299 -0
- package/dist/tools/ReadDataTool.js.map +1 -0
- package/dist/tools/RelationshipInspectorTool.d.ts +46 -0
- package/dist/tools/RelationshipInspectorTool.d.ts.map +1 -0
- package/dist/tools/RelationshipInspectorTool.js +155 -0
- package/dist/tools/RelationshipInspectorTool.js.map +1 -0
- package/dist/tools/RunScriptTool.d.ts +215 -0
- package/dist/tools/RunScriptTool.d.ts.map +1 -0
- package/dist/tools/RunScriptTool.js +177 -0
- package/dist/tools/RunScriptTool.js.map +1 -0
- package/dist/tools/SearchSchemaTool.d.ts +88 -0
- package/dist/tools/SearchSchemaTool.d.ts.map +1 -0
- package/dist/tools/SearchSchemaTool.js +236 -0
- package/dist/tools/SearchSchemaTool.js.map +1 -0
- package/dist/tools/TestConnectionTool.d.ts +36 -0
- package/dist/tools/TestConnectionTool.d.ts.map +1 -0
- package/dist/tools/TestConnectionTool.js +155 -0
- package/dist/tools/TestConnectionTool.js.map +1 -0
- package/dist/tools/ValidateEnvironmentConfigTool.d.ts +37 -0
- package/dist/tools/ValidateEnvironmentConfigTool.d.ts.map +1 -0
- package/dist/tools/ValidateEnvironmentConfigTool.js +230 -0
- package/dist/tools/ValidateEnvironmentConfigTool.js.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# MSSQL MCP Reader
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@connorbritain/mssql-mcp-reader)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
**Read-only Model Context Protocol server for Microsoft SQL Server.**
|
|
7
|
+
|
|
8
|
+
Safe schema discovery, profiling, and querying with zero risk of data modification. Ideal for analysts, auditors, and anyone who needs database exploration without write access.
|
|
9
|
+
|
|
10
|
+
## Package Tiers
|
|
11
|
+
|
|
12
|
+
| Package | npm | Tools | Use Case |
|
|
13
|
+
|---------|-----|-------|----------|
|
|
14
|
+
| **mssql-mcp-reader** (this) | `@connorbritain/mssql-mcp-reader` | 14 read-only | Analysts, auditors, safe exploration |
|
|
15
|
+
| **[mssql-mcp-writer](https://github.com/ConnorBritain/mssql-mcp-writer)** | `@connorbritain/mssql-mcp-writer` | 17 (reader + data ops) | Data engineers, ETL developers |
|
|
16
|
+
| **[mssql-mcp-server](https://github.com/ConnorBritain/mssql-mcp-server)** | `@connorbritain/mssql-mcp-server` | 20 (all tools) | DBAs, full admin access |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Tools Included
|
|
21
|
+
|
|
22
|
+
| Category | Tools |
|
|
23
|
+
|----------|-------|
|
|
24
|
+
| **Discovery** | `search_schema`, `describe_table`, `list_table`, `list_databases`, `list_environments` |
|
|
25
|
+
| **Profiling** | `profile_table`, `inspect_relationships`, `inspect_dependencies`, `explain_query` |
|
|
26
|
+
| **Querying** | `read_data` (SELECT only) |
|
|
27
|
+
| **Scripts** | `list_scripts`, `run_script` (readonly scripts only) |
|
|
28
|
+
| **Operations** | `test_connection`, `validate_environment_config` |
|
|
29
|
+
|
|
30
|
+
**Not included:** `insert_data`, `update_data`, `delete_data`, `create_table`, `create_index`, `drop_table`
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g @connorbritain/mssql-mcp-reader@latest
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### MCP Client Configuration
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"mssql": {
|
|
48
|
+
"command": "npx",
|
|
49
|
+
"args": ["@connorbritain/mssql-mcp-reader@latest"],
|
|
50
|
+
"env": {
|
|
51
|
+
"SERVER_NAME": "127.0.0.1",
|
|
52
|
+
"DATABASE_NAME": "mydb",
|
|
53
|
+
"SQL_AUTH_MODE": "sql",
|
|
54
|
+
"SQL_USERNAME": "readonly_user",
|
|
55
|
+
"SQL_PASSWORD": "YourPassword123"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
| Variable | Required | Notes |
|
|
67
|
+
|----------|----------|-------|
|
|
68
|
+
| `SERVER_NAME` | Yes | SQL Server hostname/IP |
|
|
69
|
+
| `DATABASE_NAME` | Yes | Target database |
|
|
70
|
+
| `SQL_AUTH_MODE` | | `sql`, `windows`, or `aad` (default: `aad`) |
|
|
71
|
+
| `SQL_USERNAME` / `SQL_PASSWORD` | | Required for `sql`/`windows` modes |
|
|
72
|
+
| `ENVIRONMENTS_CONFIG_PATH` | | Path to multi-environment JSON config |
|
|
73
|
+
| `SCRIPTS_PATH` | | Path to named SQL scripts directory |
|
|
74
|
+
| `AUDIT_LOG_PATH` | | Custom audit log path |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Features
|
|
79
|
+
|
|
80
|
+
All packages in the MSSQL MCP family share:
|
|
81
|
+
|
|
82
|
+
- **Multi-environment support** - Named database environments (prod, staging, dev) with per-environment policies
|
|
83
|
+
- **Governance controls** - `allowedTools`, `deniedTools`, `allowedSchemas`, `deniedSchemas`, `requireApproval`
|
|
84
|
+
- **Audit logging** - JSON Lines logs with session IDs and auto-redaction
|
|
85
|
+
- **Secret management** - `${secret:NAME}` placeholders for secure credential handling
|
|
86
|
+
- **Named SQL scripts** - Pre-approved parameterized queries with governance controls
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Documentation
|
|
91
|
+
|
|
92
|
+
Full documentation, configuration examples, and governance details are available in the main repository:
|
|
93
|
+
|
|
94
|
+
**[MSSQL MCP Server Documentation](https://github.com/ConnorBritain/mssql-mcp-server#readme)**
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT License. See [LICENSE](./LICENSE) for details.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
**Repository:** https://github.com/ConnorBritain/mssql-mcp-reader
|
|
105
|
+
**Issues:** https://github.com/ConnorBritain/mssql-mcp-reader/issues
|
|
106
|
+
**npm:** https://www.npmjs.com/package/@connorbritain/mssql-mcp-reader
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type AuditLevel = "none" | "basic" | "verbose";
|
|
2
|
+
export interface AuditLogEntry {
|
|
3
|
+
timestamp: string;
|
|
4
|
+
toolName: string;
|
|
5
|
+
environment?: string;
|
|
6
|
+
arguments?: Record<string, any>;
|
|
7
|
+
result?: {
|
|
8
|
+
success: boolean;
|
|
9
|
+
recordCount?: number;
|
|
10
|
+
error?: string;
|
|
11
|
+
data?: any;
|
|
12
|
+
};
|
|
13
|
+
durationMs?: number;
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
userId?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class AuditLogger {
|
|
18
|
+
private readonly logFilePath;
|
|
19
|
+
private readonly enabled;
|
|
20
|
+
private readonly redactSensitiveData;
|
|
21
|
+
constructor();
|
|
22
|
+
private ensureLogDirectory;
|
|
23
|
+
private redactArguments;
|
|
24
|
+
log(entry: AuditLogEntry): void;
|
|
25
|
+
logToolInvocation(toolName: string, args: any, result: any, durationMs: number, options?: {
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
userId?: string;
|
|
28
|
+
environment?: string;
|
|
29
|
+
auditLevel?: AuditLevel;
|
|
30
|
+
}): void;
|
|
31
|
+
/**
|
|
32
|
+
* Truncate result data for verbose logging to prevent huge log entries
|
|
33
|
+
*/
|
|
34
|
+
private truncateResultData;
|
|
35
|
+
}
|
|
36
|
+
export declare const auditLogger: AuditLogger;
|
|
37
|
+
//# sourceMappingURL=AuditLogger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuditLogger.d.ts","sourceRoot":"","sources":["../../src/audit/AuditLogger.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE;QACP,OAAO,EAAE,OAAO,CAAC;QACjB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,GAAG,CAAC;KACZ,CAAC;IACF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAU;;IAoB9C,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,eAAe;IA6BvB,GAAG,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAkB/B,iBAAiB,CACf,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,GAAG,EACX,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QACR,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,UAAU,CAAC;KACzB,GACA,IAAI;IA+CP;;OAEG;IACH,OAAO,CAAC,kBAAkB;CA2B3B;AAGD,eAAO,MAAM,WAAW,aAAoB,CAAC"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
export class AuditLogger {
|
|
4
|
+
constructor() {
|
|
5
|
+
// Read config from env vars
|
|
6
|
+
const logPath = process.env.AUDIT_LOG_PATH;
|
|
7
|
+
this.enabled = process.env.AUDIT_LOGGING !== "false"; // Enabled by default
|
|
8
|
+
this.redactSensitiveData = process.env.AUDIT_REDACT_SENSITIVE !== "false"; // Redact by default
|
|
9
|
+
if (this.enabled && logPath) {
|
|
10
|
+
this.logFilePath = path.resolve(logPath);
|
|
11
|
+
this.ensureLogDirectory();
|
|
12
|
+
}
|
|
13
|
+
else if (this.enabled) {
|
|
14
|
+
// Default to logs/audit.jsonl in the project root
|
|
15
|
+
this.logFilePath = path.resolve(process.cwd(), "logs", "audit.jsonl");
|
|
16
|
+
this.ensureLogDirectory();
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
this.logFilePath = "";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
ensureLogDirectory() {
|
|
23
|
+
if (!this.logFilePath)
|
|
24
|
+
return;
|
|
25
|
+
const dir = path.dirname(this.logFilePath);
|
|
26
|
+
if (!fs.existsSync(dir)) {
|
|
27
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
redactArguments(args) {
|
|
31
|
+
if (!this.redactSensitiveData) {
|
|
32
|
+
return args;
|
|
33
|
+
}
|
|
34
|
+
const redacted = { ...args };
|
|
35
|
+
const sensitiveKeys = [
|
|
36
|
+
"password",
|
|
37
|
+
"secret",
|
|
38
|
+
"token",
|
|
39
|
+
"key",
|
|
40
|
+
"authorization",
|
|
41
|
+
"auth",
|
|
42
|
+
"credential",
|
|
43
|
+
];
|
|
44
|
+
for (const [key, value] of Object.entries(redacted)) {
|
|
45
|
+
const lowerKey = key.toLowerCase();
|
|
46
|
+
if (sensitiveKeys.some((sensitive) => lowerKey.includes(sensitive))) {
|
|
47
|
+
redacted[key] = "[REDACTED]";
|
|
48
|
+
}
|
|
49
|
+
else if (typeof value === "string" && value.length > 500) {
|
|
50
|
+
// Truncate very long strings (likely large query results)
|
|
51
|
+
redacted[key] = value.substring(0, 500) + "... [TRUNCATED]";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return redacted;
|
|
55
|
+
}
|
|
56
|
+
log(entry) {
|
|
57
|
+
if (!this.enabled || !this.logFilePath) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const logEntry = {
|
|
62
|
+
...entry,
|
|
63
|
+
arguments: entry.arguments ? this.redactArguments(entry.arguments) : undefined,
|
|
64
|
+
};
|
|
65
|
+
const logLine = JSON.stringify(logEntry) + "\n";
|
|
66
|
+
fs.appendFileSync(this.logFilePath, logLine, { encoding: "utf-8" });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error("Failed to write audit log:", error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
logToolInvocation(toolName, args, result, durationMs, options) {
|
|
73
|
+
const auditLevel = options?.auditLevel ?? "basic";
|
|
74
|
+
// Skip logging entirely for 'none' level
|
|
75
|
+
if (auditLevel === "none") {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Basic level: minimal info (tool name, success, timing, environment)
|
|
79
|
+
if (auditLevel === "basic") {
|
|
80
|
+
const entry = {
|
|
81
|
+
timestamp: new Date().toISOString(),
|
|
82
|
+
toolName,
|
|
83
|
+
environment: options?.environment,
|
|
84
|
+
result: {
|
|
85
|
+
success: result?.success ?? false,
|
|
86
|
+
recordCount: result?.recordCount ?? result?.rowsAffected,
|
|
87
|
+
error: result?.error,
|
|
88
|
+
},
|
|
89
|
+
durationMs: Math.round(durationMs),
|
|
90
|
+
sessionId: options?.sessionId,
|
|
91
|
+
userId: options?.userId,
|
|
92
|
+
};
|
|
93
|
+
this.log(entry);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Verbose level: full arguments and result data
|
|
97
|
+
const entry = {
|
|
98
|
+
timestamp: new Date().toISOString(),
|
|
99
|
+
toolName,
|
|
100
|
+
environment: options?.environment,
|
|
101
|
+
arguments: args || {},
|
|
102
|
+
result: {
|
|
103
|
+
success: result?.success ?? false,
|
|
104
|
+
recordCount: result?.recordCount ?? result?.rowsAffected,
|
|
105
|
+
error: result?.error,
|
|
106
|
+
data: this.truncateResultData(result?.data),
|
|
107
|
+
},
|
|
108
|
+
durationMs: Math.round(durationMs),
|
|
109
|
+
sessionId: options?.sessionId,
|
|
110
|
+
userId: options?.userId,
|
|
111
|
+
};
|
|
112
|
+
this.log(entry);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Truncate result data for verbose logging to prevent huge log entries
|
|
116
|
+
*/
|
|
117
|
+
truncateResultData(data) {
|
|
118
|
+
if (!data)
|
|
119
|
+
return undefined;
|
|
120
|
+
// If it's an array, limit to first 10 items
|
|
121
|
+
if (Array.isArray(data)) {
|
|
122
|
+
if (data.length > 10) {
|
|
123
|
+
return {
|
|
124
|
+
_truncated: true,
|
|
125
|
+
_totalCount: data.length,
|
|
126
|
+
items: data.slice(0, 10),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return data;
|
|
130
|
+
}
|
|
131
|
+
// If it's a large object (stringified > 10KB), truncate
|
|
132
|
+
const stringified = JSON.stringify(data);
|
|
133
|
+
if (stringified.length > 10000) {
|
|
134
|
+
return {
|
|
135
|
+
_truncated: true,
|
|
136
|
+
_originalSize: stringified.length,
|
|
137
|
+
preview: stringified.substring(0, 1000) + "...",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return data;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Singleton instance
|
|
144
|
+
export const auditLogger = new AuditLogger();
|
|
145
|
+
//# sourceMappingURL=AuditLogger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuditLogger.js","sourceRoot":"","sources":["../../src/audit/AuditLogger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import sql from "mssql";
|
|
2
|
+
export type AccessLevel = "server" | "database";
|
|
3
|
+
export type TierLevel = "reader" | "writer" | "admin";
|
|
4
|
+
export type AuditLevel = "none" | "basic" | "verbose";
|
|
5
|
+
export interface EnvironmentConfig {
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
server: string;
|
|
9
|
+
database: string;
|
|
10
|
+
port?: number;
|
|
11
|
+
authMode: "sql" | "windows" | "aad";
|
|
12
|
+
username?: string;
|
|
13
|
+
password?: string;
|
|
14
|
+
domain?: string;
|
|
15
|
+
trustServerCertificate?: boolean;
|
|
16
|
+
connectionTimeout?: number;
|
|
17
|
+
readonly?: boolean;
|
|
18
|
+
allowedTools?: string[];
|
|
19
|
+
deniedTools?: string[];
|
|
20
|
+
maxRowsDefault?: number;
|
|
21
|
+
requireApproval?: boolean;
|
|
22
|
+
auditLevel?: AuditLevel;
|
|
23
|
+
accessLevel?: AccessLevel;
|
|
24
|
+
allowedDatabases?: string[] | "*";
|
|
25
|
+
deniedDatabases?: string[];
|
|
26
|
+
allowedSchemas?: string[];
|
|
27
|
+
deniedSchemas?: string[];
|
|
28
|
+
tier?: TierLevel;
|
|
29
|
+
}
|
|
30
|
+
export interface EnvironmentsConfig {
|
|
31
|
+
defaultEnvironment?: string;
|
|
32
|
+
environments: EnvironmentConfig[];
|
|
33
|
+
scriptsPath?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare class EnvironmentManager {
|
|
36
|
+
private readonly environments;
|
|
37
|
+
private defaultEnvironment?;
|
|
38
|
+
private readonly connections;
|
|
39
|
+
constructor(configPath?: string);
|
|
40
|
+
private loadFromFile;
|
|
41
|
+
private loadFromEnvVars;
|
|
42
|
+
getEnvironment(name?: string): EnvironmentConfig;
|
|
43
|
+
listEnvironments(): EnvironmentConfig[];
|
|
44
|
+
/**
|
|
45
|
+
* Check if the environment allows access to a specific database.
|
|
46
|
+
* For database-level access, only the configured database is allowed.
|
|
47
|
+
* For server-level access, checks allowedDatabases/deniedDatabases.
|
|
48
|
+
*/
|
|
49
|
+
isDatabaseAllowed(environmentName: string | undefined, databaseName: string): {
|
|
50
|
+
allowed: boolean;
|
|
51
|
+
reason?: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Check if a schema.table reference is allowed based on allowedSchemas/deniedSchemas.
|
|
55
|
+
* Pattern matching supports wildcards (e.g., "audit.*", "*.sensitive_*")
|
|
56
|
+
*/
|
|
57
|
+
isSchemaAllowed(environmentName: string | undefined, schemaName: string, tableName?: string): {
|
|
58
|
+
allowed: boolean;
|
|
59
|
+
reason?: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Simple wildcard pattern matching (supports * as wildcard)
|
|
63
|
+
*/
|
|
64
|
+
private matchesPattern;
|
|
65
|
+
getConnection(environmentName?: string): Promise<sql.ConnectionPool>;
|
|
66
|
+
private createSqlConfig;
|
|
67
|
+
closeAll(): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
export declare function getEnvironmentManager(): EnvironmentManager;
|
|
70
|
+
//# sourceMappingURL=EnvironmentManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EnvironmentManager.d.ts","sourceRoot":"","sources":["../../src/config/EnvironmentManager.ts"],"names":[],"mappings":"AAGA,OAAO,GAAG,MAAM,OAAO,CAAC;AAExB,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC;AAChD,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AACtD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAG3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,UAAU,CAAC,EAAE,UAAU,CAAC;IAGxB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;IAClC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAG3B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAGzB,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,iBAAiB,EAAE,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAmCD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiC;IAC9D,OAAO,CAAC,kBAAkB,CAAC,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA8D;gBAE9E,UAAU,CAAC,EAAE,MAAM;IAa/B,OAAO,CAAC,YAAY;IA2BpB,OAAO,CAAC,eAAe;IA+BvB,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,iBAAiB;IAahD,gBAAgB,IAAI,iBAAiB,EAAE;IAIvC;;;;OAIG;IACH,iBAAiB,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IA4CnH;;;OAGG;IACH,eAAe,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAiCnI;;OAEG;IACH,OAAO,CAAC,cAAc;IAQhB,aAAa,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;YA2B5D,eAAe;IAoFvB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAShC;AAKD,wBAAgB,qBAAqB,IAAI,kBAAkB,CAM1D"}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { InteractiveBrowserCredential } from "@azure/identity";
|
|
4
|
+
import sql from "mssql";
|
|
5
|
+
/**
|
|
6
|
+
* Resolves secret placeholders in the format ${secret:NAME}
|
|
7
|
+
* Currently supports environment variables; extensible for Key Vault, etc.
|
|
8
|
+
*/
|
|
9
|
+
function resolveSecrets(value) {
|
|
10
|
+
if (!value)
|
|
11
|
+
return value;
|
|
12
|
+
const secretPattern = /\$\{secret:([^}]+)\}/g;
|
|
13
|
+
return value.replace(secretPattern, (match, secretName) => {
|
|
14
|
+
const envValue = process.env[secretName];
|
|
15
|
+
if (envValue === undefined) {
|
|
16
|
+
console.warn(`Secret '${secretName}' not found in environment variables`);
|
|
17
|
+
return match; // Return original placeholder if not found
|
|
18
|
+
}
|
|
19
|
+
return envValue;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Recursively resolves secrets in an object's string values
|
|
24
|
+
*/
|
|
25
|
+
function resolveSecretsInConfig(config) {
|
|
26
|
+
const resolved = { ...config };
|
|
27
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
resolved[key] = resolveSecrets(value);
|
|
30
|
+
}
|
|
31
|
+
else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
32
|
+
resolved[key] = resolveSecretsInConfig(value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
export class EnvironmentManager {
|
|
38
|
+
constructor(configPath) {
|
|
39
|
+
this.environments = new Map();
|
|
40
|
+
this.connections = new Map();
|
|
41
|
+
// Try to load from config file first
|
|
42
|
+
if (configPath) {
|
|
43
|
+
this.loadFromFile(configPath);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Fall back to environment variables for single environment
|
|
47
|
+
this.loadFromEnvVars();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
loadFromFile(configPath) {
|
|
51
|
+
try {
|
|
52
|
+
const resolvedPath = path.resolve(configPath);
|
|
53
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
54
|
+
console.warn(`Environment config file not found at ${resolvedPath}, falling back to env vars`);
|
|
55
|
+
this.loadFromEnvVars();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const configContent = fs.readFileSync(resolvedPath, "utf-8");
|
|
59
|
+
const config = JSON.parse(configContent);
|
|
60
|
+
this.defaultEnvironment = config.defaultEnvironment;
|
|
61
|
+
for (const env of config.environments) {
|
|
62
|
+
// Resolve any secret placeholders in the config
|
|
63
|
+
const resolvedEnv = resolveSecretsInConfig(env);
|
|
64
|
+
this.environments.set(resolvedEnv.name, resolvedEnv);
|
|
65
|
+
}
|
|
66
|
+
console.log(`Loaded ${this.environments.size} environment(s) from ${resolvedPath}`);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error(`Failed to load environment config: ${error}`);
|
|
70
|
+
this.loadFromEnvVars();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
loadFromEnvVars() {
|
|
74
|
+
const server = process.env.SERVER_NAME;
|
|
75
|
+
const database = process.env.DATABASE_NAME;
|
|
76
|
+
if (!server || !database) {
|
|
77
|
+
throw new Error("No environment config file provided and SERVER_NAME/DATABASE_NAME env vars not set");
|
|
78
|
+
}
|
|
79
|
+
const defaultEnv = {
|
|
80
|
+
name: "default",
|
|
81
|
+
server,
|
|
82
|
+
database,
|
|
83
|
+
port: process.env.SQL_PORT ? parseInt(process.env.SQL_PORT, 10) : undefined,
|
|
84
|
+
authMode: process.env.SQL_AUTH_MODE?.toLowerCase() ?? "aad",
|
|
85
|
+
username: process.env.SQL_USERNAME,
|
|
86
|
+
password: process.env.SQL_PASSWORD,
|
|
87
|
+
domain: process.env.SQL_DOMAIN,
|
|
88
|
+
trustServerCertificate: process.env.TRUST_SERVER_CERTIFICATE?.toLowerCase() === "true",
|
|
89
|
+
connectionTimeout: process.env.CONNECTION_TIMEOUT
|
|
90
|
+
? parseInt(process.env.CONNECTION_TIMEOUT, 10)
|
|
91
|
+
: 30,
|
|
92
|
+
readonly: process.env.READONLY === "true",
|
|
93
|
+
};
|
|
94
|
+
this.environments.set("default", defaultEnv);
|
|
95
|
+
this.defaultEnvironment = "default";
|
|
96
|
+
console.log("Loaded default environment from environment variables");
|
|
97
|
+
}
|
|
98
|
+
getEnvironment(name) {
|
|
99
|
+
const targetName = name || this.defaultEnvironment || "default";
|
|
100
|
+
const env = this.environments.get(targetName);
|
|
101
|
+
if (!env) {
|
|
102
|
+
throw new Error(`Environment '${targetName}' not found. Available: ${Array.from(this.environments.keys()).join(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
return env;
|
|
105
|
+
}
|
|
106
|
+
listEnvironments() {
|
|
107
|
+
return Array.from(this.environments.values());
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if the environment allows access to a specific database.
|
|
111
|
+
* For database-level access, only the configured database is allowed.
|
|
112
|
+
* For server-level access, checks allowedDatabases/deniedDatabases.
|
|
113
|
+
*/
|
|
114
|
+
isDatabaseAllowed(environmentName, databaseName) {
|
|
115
|
+
const env = this.getEnvironment(environmentName);
|
|
116
|
+
const accessLevel = env.accessLevel ?? "database";
|
|
117
|
+
// Database-level access: only the configured database is allowed
|
|
118
|
+
if (accessLevel === "database") {
|
|
119
|
+
if (databaseName.toLowerCase() !== env.database.toLowerCase()) {
|
|
120
|
+
return {
|
|
121
|
+
allowed: false,
|
|
122
|
+
reason: `Environment '${env.name}' has database-level access and is restricted to database '${env.database}'. Cannot access '${databaseName}'.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return { allowed: true };
|
|
126
|
+
}
|
|
127
|
+
// Server-level access: check allow/deny lists
|
|
128
|
+
const deniedDatabases = env.deniedDatabases ?? [];
|
|
129
|
+
const allowedDatabases = env.allowedDatabases;
|
|
130
|
+
// Check denied list first (takes precedence)
|
|
131
|
+
if (deniedDatabases.some((db) => db.toLowerCase() === databaseName.toLowerCase())) {
|
|
132
|
+
return {
|
|
133
|
+
allowed: false,
|
|
134
|
+
reason: `Database '${databaseName}' is in the denied list for environment '${env.name}'.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Check allowed list
|
|
138
|
+
if (allowedDatabases === "*") {
|
|
139
|
+
return { allowed: true };
|
|
140
|
+
}
|
|
141
|
+
if (Array.isArray(allowedDatabases) && allowedDatabases.length > 0) {
|
|
142
|
+
if (!allowedDatabases.some((db) => db.toLowerCase() === databaseName.toLowerCase())) {
|
|
143
|
+
return {
|
|
144
|
+
allowed: false,
|
|
145
|
+
reason: `Database '${databaseName}' is not in the allowed list for environment '${env.name}'. Allowed: ${allowedDatabases.join(", ")}.`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { allowed: true };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Check if a schema.table reference is allowed based on allowedSchemas/deniedSchemas.
|
|
153
|
+
* Pattern matching supports wildcards (e.g., "audit.*", "*.sensitive_*")
|
|
154
|
+
*/
|
|
155
|
+
isSchemaAllowed(environmentName, schemaName, tableName) {
|
|
156
|
+
const env = this.getEnvironment(environmentName);
|
|
157
|
+
const fullRef = tableName ? `${schemaName}.${tableName}` : schemaName;
|
|
158
|
+
const deniedSchemas = env.deniedSchemas ?? [];
|
|
159
|
+
const allowedSchemas = env.allowedSchemas;
|
|
160
|
+
// Check denied patterns first
|
|
161
|
+
for (const pattern of deniedSchemas) {
|
|
162
|
+
if (this.matchesPattern(fullRef, pattern) || this.matchesPattern(schemaName, pattern)) {
|
|
163
|
+
return {
|
|
164
|
+
allowed: false,
|
|
165
|
+
reason: `Schema/table '${fullRef}' matches denied pattern '${pattern}' in environment '${env.name}'.`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// If allowedSchemas is specified, check against it
|
|
170
|
+
if (allowedSchemas && allowedSchemas.length > 0) {
|
|
171
|
+
const isAllowed = allowedSchemas.some((pattern) => this.matchesPattern(fullRef, pattern) || this.matchesPattern(schemaName, pattern));
|
|
172
|
+
if (!isAllowed) {
|
|
173
|
+
return {
|
|
174
|
+
allowed: false,
|
|
175
|
+
reason: `Schema/table '${fullRef}' does not match any allowed pattern in environment '${env.name}'. Allowed: ${allowedSchemas.join(", ")}.`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { allowed: true };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Simple wildcard pattern matching (supports * as wildcard)
|
|
183
|
+
*/
|
|
184
|
+
matchesPattern(value, pattern) {
|
|
185
|
+
const regexPattern = pattern
|
|
186
|
+
.replace(/[.+?^${}()|[\]\\]/g, "\\$&") // Escape special regex chars except *
|
|
187
|
+
.replace(/\*/g, ".*"); // Convert * to .*
|
|
188
|
+
const regex = new RegExp(`^${regexPattern}$`, "i");
|
|
189
|
+
return regex.test(value);
|
|
190
|
+
}
|
|
191
|
+
async getConnection(environmentName) {
|
|
192
|
+
const env = this.getEnvironment(environmentName);
|
|
193
|
+
const cached = this.connections.get(env.name);
|
|
194
|
+
// Check if we have a valid cached connection
|
|
195
|
+
if (cached &&
|
|
196
|
+
cached.pool.connected &&
|
|
197
|
+
(!cached.expiresOn || cached.expiresOn > new Date(Date.now() + 2 * 60 * 1000))) {
|
|
198
|
+
return cached.pool;
|
|
199
|
+
}
|
|
200
|
+
// Create new connection
|
|
201
|
+
const { config, expiresOn } = await this.createSqlConfig(env);
|
|
202
|
+
// Close old connection if exists
|
|
203
|
+
if (cached?.pool && cached.pool.connected) {
|
|
204
|
+
await cached.pool.close();
|
|
205
|
+
}
|
|
206
|
+
const pool = await sql.connect(config);
|
|
207
|
+
this.connections.set(env.name, { pool, expiresOn });
|
|
208
|
+
return pool;
|
|
209
|
+
}
|
|
210
|
+
async createSqlConfig(env) {
|
|
211
|
+
const baseConfig = {
|
|
212
|
+
server: env.server,
|
|
213
|
+
database: env.database,
|
|
214
|
+
port: env.port,
|
|
215
|
+
connectionTimeout: (env.connectionTimeout || 30) * 1000,
|
|
216
|
+
};
|
|
217
|
+
if (env.authMode === "sql") {
|
|
218
|
+
if (!env.username || !env.password) {
|
|
219
|
+
throw new Error(`Environment '${env.name}' requires username and password for SQL auth`);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
config: {
|
|
223
|
+
...baseConfig,
|
|
224
|
+
user: env.username,
|
|
225
|
+
password: env.password,
|
|
226
|
+
options: {
|
|
227
|
+
encrypt: false,
|
|
228
|
+
trustServerCertificate: env.trustServerCertificate ?? false,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (env.authMode === "windows") {
|
|
234
|
+
if (!env.username || !env.password) {
|
|
235
|
+
throw new Error(`Environment '${env.name}' requires username and password for Windows auth`);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
config: {
|
|
239
|
+
...baseConfig,
|
|
240
|
+
options: {
|
|
241
|
+
encrypt: false,
|
|
242
|
+
trustServerCertificate: env.trustServerCertificate ?? false,
|
|
243
|
+
},
|
|
244
|
+
authentication: {
|
|
245
|
+
type: "ntlm",
|
|
246
|
+
options: {
|
|
247
|
+
userName: env.username,
|
|
248
|
+
password: env.password,
|
|
249
|
+
domain: env.domain || "",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// Azure AD auth
|
|
256
|
+
const credential = new InteractiveBrowserCredential({
|
|
257
|
+
redirectUri: "http://localhost",
|
|
258
|
+
});
|
|
259
|
+
const accessToken = await credential.getToken("https://database.windows.net/.default");
|
|
260
|
+
if (!accessToken?.token) {
|
|
261
|
+
throw new Error(`Failed to acquire Azure AD token for environment '${env.name}'`);
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
config: {
|
|
265
|
+
...baseConfig,
|
|
266
|
+
options: {
|
|
267
|
+
encrypt: true,
|
|
268
|
+
trustServerCertificate: env.trustServerCertificate ?? false,
|
|
269
|
+
},
|
|
270
|
+
authentication: {
|
|
271
|
+
type: "azure-active-directory-access-token",
|
|
272
|
+
options: {
|
|
273
|
+
token: accessToken.token,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
expiresOn: accessToken?.expiresOnTimestamp
|
|
278
|
+
? new Date(accessToken.expiresOnTimestamp)
|
|
279
|
+
: new Date(Date.now() + 30 * 60 * 1000),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
async closeAll() {
|
|
283
|
+
for (const [name, { pool }] of this.connections.entries()) {
|
|
284
|
+
if (pool.connected) {
|
|
285
|
+
await pool.close();
|
|
286
|
+
console.log(`Closed connection for environment '${name}'`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this.connections.clear();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Singleton instance
|
|
293
|
+
let environmentManager;
|
|
294
|
+
export function getEnvironmentManager() {
|
|
295
|
+
if (!environmentManager) {
|
|
296
|
+
const configPath = process.env.ENVIRONMENTS_CONFIG_PATH;
|
|
297
|
+
environmentManager = new EnvironmentManager(configPath);
|
|
298
|
+
}
|
|
299
|
+
return environmentManager;
|
|
300
|
+
}
|
|
301
|
+
//# sourceMappingURL=EnvironmentManager.js.map
|