@simonsbs/keylore 1.0.0-rc4
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/.env.example +64 -0
- package/LICENSE +176 -0
- package/NOTICE +5 -0
- package/README.md +424 -0
- package/bin/keylore-http.js +3 -0
- package/bin/keylore-stdio.js +3 -0
- package/data/auth-clients.json +54 -0
- package/data/catalog.json +53 -0
- package/data/policies.json +25 -0
- package/dist/adapters/adapter-registry.js +143 -0
- package/dist/adapters/aws-secrets-manager-adapter.js +99 -0
- package/dist/adapters/command-runner.js +17 -0
- package/dist/adapters/env-secret-adapter.js +42 -0
- package/dist/adapters/gcp-secret-manager-adapter.js +129 -0
- package/dist/adapters/local-secret-adapter.js +54 -0
- package/dist/adapters/onepassword-secret-adapter.js +83 -0
- package/dist/adapters/reference-utils.js +44 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/vault-secret-adapter.js +103 -0
- package/dist/app.js +132 -0
- package/dist/cli/args.js +51 -0
- package/dist/cli/run.js +483 -0
- package/dist/cli.js +18 -0
- package/dist/config.js +295 -0
- package/dist/domain/types.js +967 -0
- package/dist/http/admin-ui.js +3010 -0
- package/dist/http/server.js +1210 -0
- package/dist/index.js +40 -0
- package/dist/mcp/create-server.js +388 -0
- package/dist/mcp/stdio.js +7 -0
- package/dist/repositories/credential-repository.js +109 -0
- package/dist/repositories/interfaces.js +1 -0
- package/dist/repositories/json-file.js +20 -0
- package/dist/repositories/pg-access-token-repository.js +118 -0
- package/dist/repositories/pg-approval-repository.js +157 -0
- package/dist/repositories/pg-audit-log.js +62 -0
- package/dist/repositories/pg-auth-client-repository.js +98 -0
- package/dist/repositories/pg-authorization-code-repository.js +95 -0
- package/dist/repositories/pg-break-glass-repository.js +174 -0
- package/dist/repositories/pg-credential-repository.js +163 -0
- package/dist/repositories/pg-oauth-client-assertion-repository.js +25 -0
- package/dist/repositories/pg-policy-repository.js +62 -0
- package/dist/repositories/pg-refresh-token-repository.js +125 -0
- package/dist/repositories/pg-rotation-run-repository.js +127 -0
- package/dist/repositories/pg-tenant-repository.js +56 -0
- package/dist/repositories/policy-repository.js +24 -0
- package/dist/runtime/sandbox-runner.js +114 -0
- package/dist/services/access-fingerprint.js +13 -0
- package/dist/services/approval-service.js +148 -0
- package/dist/services/audit-log.js +38 -0
- package/dist/services/auth-context.js +43 -0
- package/dist/services/auth-secrets.js +14 -0
- package/dist/services/auth-service.js +784 -0
- package/dist/services/backup-service.js +610 -0
- package/dist/services/break-glass-service.js +207 -0
- package/dist/services/broker-service.js +557 -0
- package/dist/services/core-mode-service.js +154 -0
- package/dist/services/egress-policy.js +119 -0
- package/dist/services/local-secret-store.js +119 -0
- package/dist/services/maintenance-service.js +99 -0
- package/dist/services/notification-service.js +83 -0
- package/dist/services/policy-engine.js +85 -0
- package/dist/services/rate-limit-service.js +80 -0
- package/dist/services/rotation-service.js +271 -0
- package/dist/services/telemetry.js +149 -0
- package/dist/services/tenant-service.js +127 -0
- package/dist/services/trace-export-service.js +126 -0
- package/dist/services/trace-service.js +87 -0
- package/dist/storage/bootstrap.js +68 -0
- package/dist/storage/database.js +39 -0
- package/dist/storage/in-memory-database.js +40 -0
- package/dist/storage/migrations.js +27 -0
- package/migrations/001_init.sql +49 -0
- package/migrations/002_phase2_auth.sql +53 -0
- package/migrations/003_v05_operations.sql +9 -0
- package/migrations/004_v07_security.sql +28 -0
- package/migrations/005_v08_reviews.sql +11 -0
- package/migrations/006_v09_auth_trace_rotation.sql +51 -0
- package/migrations/007_v010_multi_tenant.sql +32 -0
- package/migrations/008_v011_auth_tenant_ops.sql +95 -0
- package/package.json +78 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { daysUntil } from "../adapters/reference-utils.js";
|
|
3
|
+
import { approvalRequestSchema, breakGlassRequestInputSchema, credentialSummarySchema, } from "../domain/types.js";
|
|
4
|
+
function summarizeCredential(credential) {
|
|
5
|
+
return credentialSummarySchema.parse({
|
|
6
|
+
...credential,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
function truncate(text, maxLength) {
|
|
10
|
+
if (text.length <= maxLength) {
|
|
11
|
+
return { text, truncated: false };
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
text: `${text.slice(0, maxLength)}\n...[truncated]`,
|
|
15
|
+
truncated: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function sanitizeHeaders(headers) {
|
|
19
|
+
if (!headers) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
const blockedHeaders = new Set(["authorization", "proxy-authorization", "cookie"]);
|
|
23
|
+
const sanitizedEntries = Object.entries(headers).filter(([name]) => !blockedHeaders.has(name.toLowerCase()));
|
|
24
|
+
return Object.fromEntries(sanitizedEntries);
|
|
25
|
+
}
|
|
26
|
+
function redactText(text, secret) {
|
|
27
|
+
return text
|
|
28
|
+
.replaceAll(secret, "[REDACTED_SECRET]")
|
|
29
|
+
.replace(/gh[pousr]_[A-Za-z0-9_]+/g, "[REDACTED_GITHUB_TOKEN]")
|
|
30
|
+
.replace(/github_pat_[A-Za-z0-9_]+/g, "[REDACTED_GITHUB_TOKEN]")
|
|
31
|
+
.replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED_TOKEN]");
|
|
32
|
+
}
|
|
33
|
+
function methodForOperation(operation) {
|
|
34
|
+
return operation === "http.post" ? "POST" : "GET";
|
|
35
|
+
}
|
|
36
|
+
function tenantAllowed(context, tenantId) {
|
|
37
|
+
return !context.tenantId || context.tenantId === tenantId;
|
|
38
|
+
}
|
|
39
|
+
function requireTenantAccess(context, tenantId) {
|
|
40
|
+
if (!tenantAllowed(context, tenantId)) {
|
|
41
|
+
throw new Error("Tenant access denied.");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function readLimitedResponseBody(response, maxBytes) {
|
|
45
|
+
if (!response.body) {
|
|
46
|
+
return { text: "", truncated: false };
|
|
47
|
+
}
|
|
48
|
+
const reader = response.body.getReader();
|
|
49
|
+
const chunks = [];
|
|
50
|
+
let total = 0;
|
|
51
|
+
let truncated = false;
|
|
52
|
+
while (true) {
|
|
53
|
+
const { done, value } = await reader.read();
|
|
54
|
+
if (done) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (!value) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
total += value.byteLength;
|
|
61
|
+
if (total > maxBytes) {
|
|
62
|
+
const allowed = value.subarray(0, Math.max(0, value.byteLength - (total - maxBytes)));
|
|
63
|
+
if (allowed.byteLength > 0) {
|
|
64
|
+
chunks.push(allowed);
|
|
65
|
+
}
|
|
66
|
+
truncated = true;
|
|
67
|
+
await reader.cancel();
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
chunks.push(value);
|
|
71
|
+
}
|
|
72
|
+
const combined = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
73
|
+
return { text: combined, truncated };
|
|
74
|
+
}
|
|
75
|
+
export class BrokerService {
|
|
76
|
+
credentials;
|
|
77
|
+
policies;
|
|
78
|
+
audit;
|
|
79
|
+
adapters;
|
|
80
|
+
policyEngine;
|
|
81
|
+
approvals;
|
|
82
|
+
breakGlass;
|
|
83
|
+
tenants;
|
|
84
|
+
sandbox;
|
|
85
|
+
validateTarget;
|
|
86
|
+
config;
|
|
87
|
+
constructor(credentials, policies, audit, adapters, policyEngine, approvals, breakGlass, tenants, sandbox, validateTarget, config) {
|
|
88
|
+
this.credentials = credentials;
|
|
89
|
+
this.policies = policies;
|
|
90
|
+
this.audit = audit;
|
|
91
|
+
this.adapters = adapters;
|
|
92
|
+
this.policyEngine = policyEngine;
|
|
93
|
+
this.approvals = approvals;
|
|
94
|
+
this.breakGlass = breakGlass;
|
|
95
|
+
this.tenants = tenants;
|
|
96
|
+
this.sandbox = sandbox;
|
|
97
|
+
this.validateTarget = validateTarget;
|
|
98
|
+
this.config = config;
|
|
99
|
+
}
|
|
100
|
+
async searchCatalog(context, input) {
|
|
101
|
+
const correlationId = randomUUID();
|
|
102
|
+
const results = (await this.credentials.search(input))
|
|
103
|
+
.filter((credential) => tenantAllowed(context, credential.tenantId))
|
|
104
|
+
.map(summarizeCredential);
|
|
105
|
+
await this.audit.record({
|
|
106
|
+
type: "catalog.search",
|
|
107
|
+
action: "catalog.search",
|
|
108
|
+
outcome: "success",
|
|
109
|
+
principal: context.principal,
|
|
110
|
+
correlationId,
|
|
111
|
+
metadata: {
|
|
112
|
+
tenantId: context.tenantId ?? null,
|
|
113
|
+
query: input.query ?? null,
|
|
114
|
+
filters: input,
|
|
115
|
+
resultCount: results.length,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
120
|
+
async listCredentials(context) {
|
|
121
|
+
return this.searchCatalog(context, { limit: 50 });
|
|
122
|
+
}
|
|
123
|
+
async countCredentials() {
|
|
124
|
+
return this.credentials.count();
|
|
125
|
+
}
|
|
126
|
+
async getCredential(context, id) {
|
|
127
|
+
const correlationId = randomUUID();
|
|
128
|
+
const credential = await this.credentials.getById(id);
|
|
129
|
+
const visibleCredential = credential && tenantAllowed(context, credential.tenantId) ? credential : undefined;
|
|
130
|
+
await this.audit.record({
|
|
131
|
+
type: "catalog.read",
|
|
132
|
+
action: "catalog.get",
|
|
133
|
+
outcome: visibleCredential ? "success" : "error",
|
|
134
|
+
tenantId: credential?.tenantId ?? context.tenantId,
|
|
135
|
+
principal: context.principal,
|
|
136
|
+
correlationId,
|
|
137
|
+
metadata: {
|
|
138
|
+
tenantId: credential?.tenantId ?? context.tenantId ?? null,
|
|
139
|
+
credentialId: id,
|
|
140
|
+
found: Boolean(visibleCredential),
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return visibleCredential ? summarizeCredential(visibleCredential) : undefined;
|
|
144
|
+
}
|
|
145
|
+
async createCredential(context, credential) {
|
|
146
|
+
if (context.tenantId && credential.tenantId !== context.tenantId) {
|
|
147
|
+
throw new Error("Tenant access denied.");
|
|
148
|
+
}
|
|
149
|
+
const tenantId = context.tenantId ?? credential.tenantId;
|
|
150
|
+
const tenant = await this.tenants.getById(tenantId);
|
|
151
|
+
if (!tenant) {
|
|
152
|
+
throw new Error(`Unknown tenant: ${tenantId}`);
|
|
153
|
+
}
|
|
154
|
+
const created = await this.credentials.create({
|
|
155
|
+
...credential,
|
|
156
|
+
tenantId,
|
|
157
|
+
});
|
|
158
|
+
await this.audit.record({
|
|
159
|
+
type: "catalog.write",
|
|
160
|
+
action: "catalog.create",
|
|
161
|
+
outcome: "success",
|
|
162
|
+
tenantId: created.tenantId,
|
|
163
|
+
principal: context.principal,
|
|
164
|
+
metadata: {
|
|
165
|
+
tenantId: created.tenantId,
|
|
166
|
+
credentialId: created.id,
|
|
167
|
+
service: created.service,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
return created;
|
|
171
|
+
}
|
|
172
|
+
async updateCredential(context, id, patch) {
|
|
173
|
+
const current = await this.credentials.getById(id);
|
|
174
|
+
if (!current) {
|
|
175
|
+
throw new Error(`Credential ${id} was not found.`);
|
|
176
|
+
}
|
|
177
|
+
requireTenantAccess(context, current.tenantId);
|
|
178
|
+
const updated = await this.credentials.update(id, patch);
|
|
179
|
+
await this.audit.record({
|
|
180
|
+
type: "catalog.write",
|
|
181
|
+
action: "catalog.update",
|
|
182
|
+
outcome: "success",
|
|
183
|
+
tenantId: updated.tenantId,
|
|
184
|
+
principal: context.principal,
|
|
185
|
+
metadata: {
|
|
186
|
+
tenantId: updated.tenantId,
|
|
187
|
+
credentialId: id,
|
|
188
|
+
fields: Object.keys(patch),
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
return updated;
|
|
192
|
+
}
|
|
193
|
+
async deleteCredential(context, id) {
|
|
194
|
+
const current = await this.credentials.getById(id);
|
|
195
|
+
if (current) {
|
|
196
|
+
requireTenantAccess(context, current.tenantId);
|
|
197
|
+
}
|
|
198
|
+
const deleted = await this.credentials.delete(id);
|
|
199
|
+
await this.audit.record({
|
|
200
|
+
type: "catalog.write",
|
|
201
|
+
action: "catalog.delete",
|
|
202
|
+
outcome: deleted ? "success" : "error",
|
|
203
|
+
tenantId: current?.tenantId ?? context.tenantId,
|
|
204
|
+
principal: context.principal,
|
|
205
|
+
metadata: {
|
|
206
|
+
tenantId: current?.tenantId ?? context.tenantId ?? null,
|
|
207
|
+
credentialId: id,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
return deleted;
|
|
211
|
+
}
|
|
212
|
+
async requestAccess(context, input) {
|
|
213
|
+
if (input.dryRun) {
|
|
214
|
+
return this.simulateAccess(context, input, "dry_run");
|
|
215
|
+
}
|
|
216
|
+
const evaluation = await this.evaluateAccess(context, input, "live");
|
|
217
|
+
if (!evaluation.credential || !evaluation.policyDecision) {
|
|
218
|
+
return evaluation.decision;
|
|
219
|
+
}
|
|
220
|
+
const { credential, policyDecision, correlationId, targetUrl } = evaluation;
|
|
221
|
+
if (!targetUrl) {
|
|
222
|
+
return evaluation.decision;
|
|
223
|
+
}
|
|
224
|
+
const breakGlassGrant = await this.breakGlass.verifyActive(context, input);
|
|
225
|
+
if (breakGlassGrant) {
|
|
226
|
+
await this.breakGlass.recordUse(context, breakGlassGrant);
|
|
227
|
+
return this.executeAllowedRequest("live", context, input, credential, { decision: "allow", reason: "Emergency break-glass grant is active.", ruleId: undefined }, correlationId, targetUrl, breakGlassGrant);
|
|
228
|
+
}
|
|
229
|
+
if (policyDecision.decision === "deny") {
|
|
230
|
+
if (input.breakGlassId) {
|
|
231
|
+
return this.toBreakGlassDeniedDecision("live", correlationId, credential);
|
|
232
|
+
}
|
|
233
|
+
return this.toDeniedDecision("live", correlationId, credential, policyDecision);
|
|
234
|
+
}
|
|
235
|
+
if (policyDecision.decision === "approval") {
|
|
236
|
+
const approved = await this.approvals.verifyApproval(context, input);
|
|
237
|
+
if (approved) {
|
|
238
|
+
return this.executeAllowedRequest("live", context, input, credential, policyDecision, correlationId, targetUrl);
|
|
239
|
+
}
|
|
240
|
+
if (input.breakGlassId) {
|
|
241
|
+
return this.toBreakGlassDeniedDecision("live", correlationId, credential);
|
|
242
|
+
}
|
|
243
|
+
const approval = await this.approvals.createPending(context, input, {
|
|
244
|
+
reason: policyDecision.reason,
|
|
245
|
+
ruleId: policyDecision.ruleId,
|
|
246
|
+
correlationId,
|
|
247
|
+
}, credential.tenantId);
|
|
248
|
+
return this.toApprovalRequiredDecision("live", correlationId, credential, policyDecision, approval);
|
|
249
|
+
}
|
|
250
|
+
return this.executeAllowedRequest("live", context, input, credential, policyDecision, correlationId, targetUrl);
|
|
251
|
+
}
|
|
252
|
+
async simulateAccess(context, input, mode = "simulation") {
|
|
253
|
+
const evaluation = await this.evaluateAccess(context, input, mode);
|
|
254
|
+
if (!evaluation.credential || !evaluation.policyDecision) {
|
|
255
|
+
return evaluation.decision;
|
|
256
|
+
}
|
|
257
|
+
const { credential, policyDecision, correlationId } = evaluation;
|
|
258
|
+
const breakGlassGrant = await this.breakGlass.verifyActive(context, input);
|
|
259
|
+
if (breakGlassGrant) {
|
|
260
|
+
return this.toAllowedDecision(mode, correlationId, credential, {
|
|
261
|
+
decision: "allow",
|
|
262
|
+
reason: "Emergency break-glass grant is active.",
|
|
263
|
+
ruleId: undefined,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (policyDecision.decision === "deny") {
|
|
267
|
+
if (input.breakGlassId) {
|
|
268
|
+
return this.toBreakGlassDeniedDecision(mode, correlationId, credential);
|
|
269
|
+
}
|
|
270
|
+
return this.toDeniedDecision(mode, correlationId, credential, policyDecision);
|
|
271
|
+
}
|
|
272
|
+
if (policyDecision.decision === "approval") {
|
|
273
|
+
const approved = await this.approvals.verifyApproval(context, input);
|
|
274
|
+
if (approved) {
|
|
275
|
+
return this.toAllowedDecision(mode, correlationId, credential, policyDecision);
|
|
276
|
+
}
|
|
277
|
+
if (input.breakGlassId) {
|
|
278
|
+
return this.toBreakGlassDeniedDecision(mode, correlationId, credential);
|
|
279
|
+
}
|
|
280
|
+
return this.toApprovalRequiredDecision(mode, correlationId, credential, policyDecision);
|
|
281
|
+
}
|
|
282
|
+
return this.toAllowedDecision(mode, correlationId, credential, policyDecision);
|
|
283
|
+
}
|
|
284
|
+
async evaluateAccess(context, input, mode) {
|
|
285
|
+
const correlationId = randomUUID();
|
|
286
|
+
const credential = await this.credentials.getById(input.credentialId);
|
|
287
|
+
const action = mode === "simulation"
|
|
288
|
+
? "access.simulate"
|
|
289
|
+
: mode === "dry_run"
|
|
290
|
+
? "access.dry_run"
|
|
291
|
+
: "access.request";
|
|
292
|
+
if (!credential || !tenantAllowed(context, credential.tenantId)) {
|
|
293
|
+
await this.audit.record({
|
|
294
|
+
type: "authz.decision",
|
|
295
|
+
action,
|
|
296
|
+
outcome: "denied",
|
|
297
|
+
tenantId: credential?.tenantId ?? context.tenantId,
|
|
298
|
+
principal: context.principal,
|
|
299
|
+
correlationId,
|
|
300
|
+
metadata: {
|
|
301
|
+
tenantId: credential?.tenantId ?? context.tenantId ?? null,
|
|
302
|
+
credentialId: input.credentialId,
|
|
303
|
+
reason: "Credential not found.",
|
|
304
|
+
mode,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
correlationId,
|
|
309
|
+
decision: {
|
|
310
|
+
decision: "denied",
|
|
311
|
+
mode,
|
|
312
|
+
reason: "Credential not found.",
|
|
313
|
+
correlationId,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
let targetUrl;
|
|
318
|
+
try {
|
|
319
|
+
targetUrl = await this.validateTarget(input.targetUrl, this.config);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
const reason = error instanceof Error ? error.message : "Target is blocked by egress policy.";
|
|
323
|
+
await this.audit.record({
|
|
324
|
+
type: "authz.decision",
|
|
325
|
+
action,
|
|
326
|
+
outcome: "denied",
|
|
327
|
+
tenantId: credential.tenantId,
|
|
328
|
+
principal: context.principal,
|
|
329
|
+
correlationId,
|
|
330
|
+
metadata: {
|
|
331
|
+
tenantId: credential.tenantId,
|
|
332
|
+
credentialId: credential.id,
|
|
333
|
+
operation: input.operation,
|
|
334
|
+
targetUrl: input.targetUrl,
|
|
335
|
+
reason,
|
|
336
|
+
mode,
|
|
337
|
+
dryRun: mode !== "live",
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
correlationId,
|
|
342
|
+
credential,
|
|
343
|
+
decision: {
|
|
344
|
+
decision: "denied",
|
|
345
|
+
mode,
|
|
346
|
+
reason,
|
|
347
|
+
correlationId,
|
|
348
|
+
credential: summarizeCredential(credential),
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const policies = await this.policies.read();
|
|
353
|
+
const decision = this.policyEngine.evaluate({
|
|
354
|
+
...policies,
|
|
355
|
+
rules: policies.rules.filter((rule) => rule.tenantId === credential.tenantId),
|
|
356
|
+
}, context.principal, context.roles, credential, input.operation, targetUrl.hostname, this.config.environment);
|
|
357
|
+
await this.audit.record({
|
|
358
|
+
type: "authz.decision",
|
|
359
|
+
action,
|
|
360
|
+
outcome: decision.decision === "allow" ? "allowed" : "denied",
|
|
361
|
+
tenantId: credential.tenantId,
|
|
362
|
+
principal: context.principal,
|
|
363
|
+
correlationId,
|
|
364
|
+
metadata: {
|
|
365
|
+
tenantId: credential.tenantId,
|
|
366
|
+
credentialId: credential.id,
|
|
367
|
+
operation: input.operation,
|
|
368
|
+
targetHost: targetUrl.hostname,
|
|
369
|
+
ruleId: decision.ruleId ?? null,
|
|
370
|
+
reason: decision.reason,
|
|
371
|
+
mode,
|
|
372
|
+
dryRun: mode !== "live",
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
correlationId,
|
|
377
|
+
credential,
|
|
378
|
+
policyDecision: decision,
|
|
379
|
+
targetUrl,
|
|
380
|
+
decision: this.toAllowedDecision(mode, correlationId, credential, decision),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async listRecentAuditEvents(context, limit = 20) {
|
|
384
|
+
return this.audit.listRecent(limit, context.tenantId);
|
|
385
|
+
}
|
|
386
|
+
async listCredentialReports(context, id) {
|
|
387
|
+
const credentials = id
|
|
388
|
+
? ((await this.credentials.getById(id)) ? [await this.credentials.getById(id)] : [])
|
|
389
|
+
: await this.credentials.list();
|
|
390
|
+
return Promise.all(credentials
|
|
391
|
+
.filter((credential) => Boolean(credential) && tenantAllowed(context, credential.tenantId))
|
|
392
|
+
.map(async (credential) => ({
|
|
393
|
+
credential: summarizeCredential(credential),
|
|
394
|
+
runtimeMode: credential.binding.injectionEnvName ? "sandbox_injection" : "proxy",
|
|
395
|
+
catalogExpiresAt: credential.expiresAt,
|
|
396
|
+
daysUntilCatalogExpiry: daysUntil(credential.expiresAt),
|
|
397
|
+
inspection: await this.adapters.inspectCredential(credential),
|
|
398
|
+
})));
|
|
399
|
+
}
|
|
400
|
+
async adapterHealth() {
|
|
401
|
+
return this.adapters.healthchecks();
|
|
402
|
+
}
|
|
403
|
+
async listApprovalRequests(context, status) {
|
|
404
|
+
return this.approvals.list(context, status);
|
|
405
|
+
}
|
|
406
|
+
async reviewApprovalRequest(context, id, status, note) {
|
|
407
|
+
return this.approvals.review(id, context, status, note);
|
|
408
|
+
}
|
|
409
|
+
async createBreakGlassRequest(context, input) {
|
|
410
|
+
const parsed = breakGlassRequestInputSchema.parse(input);
|
|
411
|
+
const credential = await this.credentials.getById(parsed.credentialId);
|
|
412
|
+
if (!credential) {
|
|
413
|
+
throw new Error("Credential not found.");
|
|
414
|
+
}
|
|
415
|
+
requireTenantAccess(context, credential.tenantId);
|
|
416
|
+
await this.validateTarget(parsed.targetUrl, this.config);
|
|
417
|
+
return this.breakGlass.createRequest(context, parsed, credential.tenantId);
|
|
418
|
+
}
|
|
419
|
+
async listBreakGlassRequests(context, filter) {
|
|
420
|
+
return this.breakGlass.list(context, filter);
|
|
421
|
+
}
|
|
422
|
+
async reviewBreakGlassRequest(context, id, status, note) {
|
|
423
|
+
return this.breakGlass.review(id, context, status, note);
|
|
424
|
+
}
|
|
425
|
+
async revokeBreakGlassRequest(context, id, note) {
|
|
426
|
+
return this.breakGlass.revoke(id, context, note);
|
|
427
|
+
}
|
|
428
|
+
async runSandboxed(context, input) {
|
|
429
|
+
const credential = await this.credentials.getById(input.credentialId);
|
|
430
|
+
if (!credential) {
|
|
431
|
+
throw new Error("Credential not found.");
|
|
432
|
+
}
|
|
433
|
+
requireTenantAccess(context, credential.tenantId);
|
|
434
|
+
const resolved = await this.adapters.resolve(credential);
|
|
435
|
+
const secretEnvName = input.secretEnvName ?? credential.binding.injectionEnvName;
|
|
436
|
+
if (!secretEnvName) {
|
|
437
|
+
throw new Error("Sandbox execution requires secretEnvName or credential.binding.injectionEnvName.");
|
|
438
|
+
}
|
|
439
|
+
const result = await this.sandbox.run(input, resolved.secret, secretEnvName);
|
|
440
|
+
await this.audit.record({
|
|
441
|
+
type: "runtime.exec",
|
|
442
|
+
action: "runtime.exec",
|
|
443
|
+
outcome: result.exitCode === 0 ? "success" : "error",
|
|
444
|
+
tenantId: credential.tenantId,
|
|
445
|
+
principal: context.principal,
|
|
446
|
+
metadata: {
|
|
447
|
+
tenantId: credential.tenantId,
|
|
448
|
+
credentialId: credential.id,
|
|
449
|
+
command: input.command,
|
|
450
|
+
args: input.args,
|
|
451
|
+
exitCode: result.exitCode,
|
|
452
|
+
timedOut: result.timedOut,
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
async executeAllowedRequest(mode, context, input, credential, decision, correlationId, targetUrl, breakGlassGrant) {
|
|
458
|
+
const resolved = await this.adapters.resolve(credential);
|
|
459
|
+
const httpResult = await this.executeProxyRequest(input, targetUrl, resolved.secret, {
|
|
460
|
+
[resolved.headerName]: resolved.headerValue,
|
|
461
|
+
});
|
|
462
|
+
await this.audit.record({
|
|
463
|
+
type: "credential.use",
|
|
464
|
+
action: "proxy.http",
|
|
465
|
+
outcome: "success",
|
|
466
|
+
tenantId: credential.tenantId,
|
|
467
|
+
principal: context.principal,
|
|
468
|
+
correlationId,
|
|
469
|
+
metadata: {
|
|
470
|
+
tenantId: credential.tenantId,
|
|
471
|
+
credentialId: credential.id,
|
|
472
|
+
operation: input.operation,
|
|
473
|
+
targetHost: targetUrl.hostname,
|
|
474
|
+
ruleId: decision.ruleId ?? null,
|
|
475
|
+
status: httpResult.status,
|
|
476
|
+
breakGlassId: breakGlassGrant?.id ?? null,
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
return {
|
|
480
|
+
decision: "allowed",
|
|
481
|
+
mode,
|
|
482
|
+
reason: decision.reason,
|
|
483
|
+
correlationId,
|
|
484
|
+
credential: summarizeCredential(credential),
|
|
485
|
+
ruleId: decision.ruleId,
|
|
486
|
+
httpResult,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
toBreakGlassDeniedDecision(mode, correlationId, credential) {
|
|
490
|
+
return {
|
|
491
|
+
decision: "denied",
|
|
492
|
+
mode,
|
|
493
|
+
reason: "Break-glass grant is invalid, expired, or does not match this request.",
|
|
494
|
+
correlationId,
|
|
495
|
+
credential: summarizeCredential(credential),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
toDeniedDecision(mode, correlationId, credential, decision) {
|
|
499
|
+
return {
|
|
500
|
+
decision: "denied",
|
|
501
|
+
mode,
|
|
502
|
+
reason: decision.reason,
|
|
503
|
+
correlationId,
|
|
504
|
+
credential: summarizeCredential(credential),
|
|
505
|
+
ruleId: decision.ruleId,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
toAllowedDecision(mode, correlationId, credential, decision) {
|
|
509
|
+
return {
|
|
510
|
+
decision: "allowed",
|
|
511
|
+
mode,
|
|
512
|
+
reason: decision.reason,
|
|
513
|
+
correlationId,
|
|
514
|
+
credential: summarizeCredential(credential),
|
|
515
|
+
ruleId: decision.ruleId,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
toApprovalRequiredDecision(mode, correlationId, credential, decision, approval) {
|
|
519
|
+
if (approval) {
|
|
520
|
+
approvalRequestSchema.parse(approval);
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
decision: "approval_required",
|
|
524
|
+
mode,
|
|
525
|
+
reason: decision.reason,
|
|
526
|
+
correlationId,
|
|
527
|
+
credential: summarizeCredential(credential),
|
|
528
|
+
ruleId: decision.ruleId,
|
|
529
|
+
approvalRequestId: approval?.id,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
async executeProxyRequest(input, targetUrl, secret, authHeaders) {
|
|
533
|
+
const userHeaders = sanitizeHeaders(input.headers);
|
|
534
|
+
const requestInit = {
|
|
535
|
+
method: methodForOperation(input.operation),
|
|
536
|
+
headers: {
|
|
537
|
+
accept: "application/json, text/plain;q=0.9, */*;q=0.8",
|
|
538
|
+
...userHeaders,
|
|
539
|
+
...authHeaders,
|
|
540
|
+
},
|
|
541
|
+
signal: AbortSignal.timeout(this.config.outboundTimeoutMs),
|
|
542
|
+
};
|
|
543
|
+
if (input.operation === "http.post" && input.payload) {
|
|
544
|
+
requestInit.body = input.payload;
|
|
545
|
+
}
|
|
546
|
+
const response = await fetch(targetUrl, requestInit);
|
|
547
|
+
const limitedBody = await readLimitedResponseBody(response, this.config.maxResponseBytes);
|
|
548
|
+
const redactedText = redactText(limitedBody.text, secret);
|
|
549
|
+
const truncated = truncate(redactedText, this.config.maxResponseBytes);
|
|
550
|
+
return {
|
|
551
|
+
status: response.status,
|
|
552
|
+
contentType: response.headers.get("content-type"),
|
|
553
|
+
bodyPreview: truncated.text,
|
|
554
|
+
bodyTruncated: limitedBody.truncated || truncated.truncated,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|