emilsoftware-utilities 2.0.0-dev.77 → 2.0.0-dev.79
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/dist/accessi/client/generated/admin.d.ts +21 -1
- package/dist/accessi/client/generated/admin.js +35 -1
- package/dist/accessi/client/generated/admin.js.map +1 -1
- package/dist/accessi/resource-server/token.js +2 -0
- package/dist/accessi/resource-server/token.js.map +1 -1
- package/dist/accessi/server/standalone.js +2 -1
- package/dist/accessi/server/standalone.js.map +1 -1
- package/dist/accessi-module/AccessiModule.d.ts +6 -0
- package/dist/accessi-module/AccessiModule.js +4 -0
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Console/console.js +2 -0
- package/dist/accessi-module/Console/generated/emilsoftware-utilities.d.ts +21 -1
- package/dist/accessi-module/Console/generated/emilsoftware-utilities.js +35 -1
- package/dist/accessi-module/Console/generated/emilsoftware-utilities.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/exportAuditOrder.d.ts +11 -0
- package/dist/accessi-module/Console/generated/model/exportAuditOrder.js +15 -0
- package/dist/accessi-module/Console/generated/model/exportAuditOrder.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/exportAuditParams.d.ts +37 -0
- package/dist/accessi-module/Console/generated/model/exportAuditParams.js +3 -0
- package/dist/accessi-module/Console/generated/model/exportAuditParams.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/index.d.ts +4 -0
- package/dist/accessi-module/Console/generated/model/index.js +4 -0
- package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/queryAuditOrder.d.ts +11 -0
- package/dist/accessi-module/Console/generated/model/queryAuditOrder.js +15 -0
- package/dist/accessi-module/Console/generated/model/queryAuditOrder.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/queryAuditParams.d.ts +37 -0
- package/dist/accessi-module/Console/generated/model/queryAuditParams.js +3 -0
- package/dist/accessi-module/Console/generated/model/queryAuditParams.js.map +1 -0
- package/dist/accessi-module/Console/wiki.js +2 -0
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/accessi-module/Controllers/AccessiAuditController.d.ts +16 -0
- package/dist/accessi-module/Controllers/AccessiAuditController.js +155 -0
- package/dist/accessi-module/Controllers/AccessiAuditController.js.map +1 -0
- package/dist/accessi-module/Controllers/AccessiIntrospectionController.js +9 -0
- package/dist/accessi-module/Controllers/AccessiIntrospectionController.js.map +1 -1
- package/dist/accessi-module/Controllers/AuthController.js +82 -8
- package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
- package/dist/accessi-module/Controllers/ConsoleEntryController.js +15 -0
- package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -1
- package/dist/accessi-module/Dtos/AuditDtos.d.ts +10 -0
- package/dist/accessi-module/Dtos/AuditDtos.js +68 -0
- package/dist/accessi-module/Dtos/AuditDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/index.d.ts +1 -0
- package/dist/accessi-module/Dtos/index.js +1 -0
- package/dist/accessi-module/Dtos/index.js.map +1 -1
- package/dist/accessi-module/Services/AuthService/AuthService.js +8 -2
- package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
- package/dist/accessi-module/index.d.ts +9 -1
- package/dist/accessi-module/index.js +27 -2
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/accessi-module/observability/metrics.d.ts +79 -0
- package/dist/accessi-module/observability/metrics.js +216 -0
- package/dist/accessi-module/observability/metrics.js.map +1 -0
- package/dist/accessi-module/observability/redisMetrics.d.ts +12 -0
- package/dist/accessi-module/observability/redisMetrics.js +44 -0
- package/dist/accessi-module/observability/redisMetrics.js.map +1 -0
- package/dist/accessi-module/security/accessTokenRevocation.js +5 -3
- package/dist/accessi-module/security/accessTokenRevocation.js.map +1 -1
- package/dist/accessi-module/security/audit.d.ts +68 -3
- package/dist/accessi-module/security/audit.js +177 -4
- package/dist/accessi-module/security/audit.js.map +1 -1
- package/dist/accessi-module/security/auditSinks.d.ts +55 -0
- package/dist/accessi-module/security/auditSinks.js +228 -0
- package/dist/accessi-module/security/auditSinks.js.map +1 -0
- package/dist/accessi-module/security/consoleEntry.js +5 -3
- package/dist/accessi-module/security/consoleEntry.js.map +1 -1
- package/dist/accessi-module/security/edgeRateLimit.js +3 -0
- package/dist/accessi-module/security/edgeRateLimit.js.map +1 -1
- package/dist/accessi-module/security/jwks.js +3 -0
- package/dist/accessi-module/security/jwks.js.map +1 -1
- package/dist/accessi-module/security/jwtTokenOptions.js +4 -0
- package/dist/accessi-module/security/jwtTokenOptions.js.map +1 -1
- package/dist/accessi-module/security/publicAuthRateLimit.js +10 -0
- package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
- package/dist/accessi-module/security/rateLimitStore.js +4 -2
- package/dist/accessi-module/security/rateLimitStore.js.map +1 -1
- package/dist/accessi-module/security/refreshTokenRotation.js +7 -5
- package/dist/accessi-module/security/refreshTokenRotation.js.map +1 -1
- package/dist/accessi-module/security/serviceTokenRateLimit.js +3 -0
- package/dist/accessi-module/security/serviceTokenRateLimit.js.map +1 -1
- package/dist/accessi-module/security/sessionRevocation.js +7 -3
- package/dist/accessi-module/security/sessionRevocation.js.map +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.79.tgz +0 -0
- package/docs/audit.md +136 -0
- package/docs/metrics.md +153 -0
- package/docs/redis-load-testing.md +88 -0
- package/package.json +4 -3
- package/dist/emilsoftware-utilities-2.0.0-dev.77.tgz +0 -0
|
@@ -1,14 +1,187 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.redactAccessiAuditValue = redactAccessiAuditValue;
|
|
4
|
+
exports.configureAccessiAudit = configureAccessiAudit;
|
|
5
|
+
exports.resetAccessiAudit = resetAccessiAudit;
|
|
6
|
+
exports.getAccessiAuditStore = getAccessiAuditStore;
|
|
3
7
|
exports.auditAccessi = auditAccessi;
|
|
4
8
|
const Logger_1 = require("../../Logger");
|
|
9
|
+
const metrics_1 = require("../observability/metrics");
|
|
10
|
+
const REDACTED = '[REDACTED]';
|
|
11
|
+
const TRUNCATED = '[TRUNCATED]';
|
|
12
|
+
const DEFAULT_REDACTED_KEYS = new Set([
|
|
13
|
+
'token',
|
|
14
|
+
'accesstoken',
|
|
15
|
+
'refreshtoken',
|
|
16
|
+
'idtoken',
|
|
17
|
+
'bearer',
|
|
18
|
+
'jwt',
|
|
19
|
+
'authorization',
|
|
20
|
+
'cookie',
|
|
21
|
+
'setcookie',
|
|
22
|
+
'password',
|
|
23
|
+
'passwordhash',
|
|
24
|
+
'hashedpassword',
|
|
25
|
+
'resettoken',
|
|
26
|
+
'secret',
|
|
27
|
+
'clientsecret',
|
|
28
|
+
'privatekey',
|
|
29
|
+
'apikey',
|
|
30
|
+
'csrf',
|
|
31
|
+
'csrftoken',
|
|
32
|
+
]);
|
|
33
|
+
const MAX_STRING_LENGTH = 512;
|
|
34
|
+
const MAX_ARRAY_LENGTH = 50;
|
|
35
|
+
const MAX_OBJECT_KEYS = 100;
|
|
36
|
+
const MAX_DEPTH = 4;
|
|
37
|
+
const PURGE_INTERVAL_MS = 60 * 60 * 1000;
|
|
38
|
+
let sinks = [];
|
|
39
|
+
let redactedKeys = new Set(DEFAULT_REDACTED_KEYS);
|
|
40
|
+
let retentionDays;
|
|
41
|
+
let loggerEnabled = true;
|
|
42
|
+
let lastPurgeAt = 0;
|
|
43
|
+
function normalizedKey(key) {
|
|
44
|
+
return key.toLowerCase().replace(/[-_\s]/g, '');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Redige ricorsivamente token, segreti, cookie e header sensibili.
|
|
48
|
+
*
|
|
49
|
+
* Limita profondita, lunghezza delle stringhe e numero di elementi per evitare
|
|
50
|
+
* che un payload applicativo finisca per intero nell'audit.
|
|
51
|
+
*/
|
|
52
|
+
function redactAccessiAuditValue(value, depth = 0) {
|
|
53
|
+
if (depth > MAX_DEPTH)
|
|
54
|
+
return TRUNCATED;
|
|
55
|
+
if (value === null || value === undefined)
|
|
56
|
+
return value;
|
|
57
|
+
if (typeof value === 'string') {
|
|
58
|
+
return value.length > MAX_STRING_LENGTH ? `${value.slice(0, MAX_STRING_LENGTH - 3)}...` : value;
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
61
|
+
return value;
|
|
62
|
+
if (typeof value === 'bigint')
|
|
63
|
+
return value.toString();
|
|
64
|
+
if (value instanceof Date)
|
|
65
|
+
return value.toISOString();
|
|
66
|
+
if (value instanceof Error) {
|
|
67
|
+
return { name: value.name, message: redactAccessiAuditValue(value.message, depth + 1) };
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
return value.slice(0, MAX_ARRAY_LENGTH).map((item) => redactAccessiAuditValue(item, depth + 1));
|
|
71
|
+
}
|
|
72
|
+
if (typeof value === 'object') {
|
|
73
|
+
const result = {};
|
|
74
|
+
for (const [key, entry] of Object.entries(value).slice(0, MAX_OBJECT_KEYS)) {
|
|
75
|
+
result[key] = redactedKeys.has(normalizedKey(key))
|
|
76
|
+
? REDACTED
|
|
77
|
+
: redactAccessiAuditValue(entry, depth + 1);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
return String(value);
|
|
82
|
+
}
|
|
83
|
+
function redactDetails(details) {
|
|
84
|
+
return redactAccessiAuditValue(details);
|
|
85
|
+
}
|
|
86
|
+
function reportSinkError(sink, error) {
|
|
87
|
+
var _a;
|
|
88
|
+
(0, metrics_1.recordAccessiMetric)('accessi_audit_sink_errors_total', 1, { sink: (_a = sink.name) !== null && _a !== void 0 ? _a : 'unknown' });
|
|
89
|
+
try {
|
|
90
|
+
new Logger_1.Logger('AccessiAudit').error(`Audit sink non disponibile${sink.name ? ` (${sink.name})` : ''}: ${error instanceof Error ? error.message : String(error)}`);
|
|
91
|
+
}
|
|
92
|
+
catch (_b) {
|
|
93
|
+
// Il logging non deve mai interrompere l'operazione applicativa.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function writeToSink(sink, record) {
|
|
97
|
+
try {
|
|
98
|
+
const result = sink.write(record);
|
|
99
|
+
if (result && typeof result.catch === 'function') {
|
|
100
|
+
result.catch((error) => reportSinkError(sink, error));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
reportSinkError(sink, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function maybePurge() {
|
|
108
|
+
if (!retentionDays || retentionDays <= 0)
|
|
109
|
+
return;
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
if (now - lastPurgeAt < PURGE_INTERVAL_MS)
|
|
112
|
+
return;
|
|
113
|
+
lastPurgeAt = now;
|
|
114
|
+
const before = new Date(now - retentionDays * 24 * 60 * 60 * 1000);
|
|
115
|
+
for (const sink of sinks) {
|
|
116
|
+
if (typeof sink.purge !== 'function')
|
|
117
|
+
continue;
|
|
118
|
+
try {
|
|
119
|
+
const result = sink.purge(before);
|
|
120
|
+
if (result && typeof result.catch === 'function') {
|
|
121
|
+
result.catch((error) => reportSinkError(sink, error));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
reportSinkError(sink, error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Configura i sink di audit del processo.
|
|
131
|
+
*
|
|
132
|
+
* Chiamato da `AccessiModule.forRoot`. Le opzioni sono globali: un solo modulo
|
|
133
|
+
* Accessi per processo e la configurazione consigliata.
|
|
134
|
+
*/
|
|
135
|
+
function configureAccessiAudit(options) {
|
|
136
|
+
var _a, _b;
|
|
137
|
+
sinks = [];
|
|
138
|
+
redactedKeys = new Set(DEFAULT_REDACTED_KEYS);
|
|
139
|
+
retentionDays = undefined;
|
|
140
|
+
loggerEnabled = true;
|
|
141
|
+
lastPurgeAt = 0;
|
|
142
|
+
for (const key of (_a = options === null || options === void 0 ? void 0 : options.redact) !== null && _a !== void 0 ? _a : [])
|
|
143
|
+
redactedKeys.add(normalizedKey(key));
|
|
144
|
+
loggerEnabled = (options === null || options === void 0 ? void 0 : options.logger) !== false;
|
|
145
|
+
if ((options === null || options === void 0 ? void 0 : options.enabled) === false)
|
|
146
|
+
return;
|
|
147
|
+
if (options === null || options === void 0 ? void 0 : options.store)
|
|
148
|
+
sinks.push(options.store);
|
|
149
|
+
for (const sink of (_b = options === null || options === void 0 ? void 0 : options.sinks) !== null && _b !== void 0 ? _b : []) {
|
|
150
|
+
if (!sinks.includes(sink))
|
|
151
|
+
sinks.push(sink);
|
|
152
|
+
}
|
|
153
|
+
const days = Number(options === null || options === void 0 ? void 0 : options.retentionDays);
|
|
154
|
+
if (Number.isFinite(days) && days > 0)
|
|
155
|
+
retentionDays = days;
|
|
156
|
+
}
|
|
157
|
+
/** Ripristina la configurazione iniziale (logger attivo, nessun sink). */
|
|
158
|
+
function resetAccessiAudit() {
|
|
159
|
+
configureAccessiAudit(undefined);
|
|
160
|
+
}
|
|
161
|
+
/** Sink interrogabile configurato, usato dagli endpoint amministrativi. */
|
|
162
|
+
function getAccessiAuditStore() {
|
|
163
|
+
const configured = sinks.find((sink) => typeof sink.query === 'function'
|
|
164
|
+
&& typeof sink.purge === 'function');
|
|
165
|
+
return configured;
|
|
166
|
+
}
|
|
5
167
|
/**
|
|
6
|
-
*
|
|
168
|
+
* Registra un evento di sicurezza.
|
|
7
169
|
*
|
|
8
|
-
* Il
|
|
9
|
-
*
|
|
170
|
+
* Il record viene redatto prima di raggiungere logger e sink. Un sink guasto
|
|
171
|
+
* non interrompe mai il flusso applicativo.
|
|
10
172
|
*/
|
|
11
173
|
function auditAccessi(event, details = {}) {
|
|
12
|
-
|
|
174
|
+
const record = Object.assign({ event, timestamp: new Date().toISOString() }, redactDetails(details !== null && details !== void 0 ? details : {}));
|
|
175
|
+
if (loggerEnabled) {
|
|
176
|
+
try {
|
|
177
|
+
new Logger_1.Logger('AccessiAudit').warning(JSON.stringify(record));
|
|
178
|
+
}
|
|
179
|
+
catch (_a) {
|
|
180
|
+
// Il logging non deve mai interrompere l'operazione applicativa.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const sink of sinks)
|
|
184
|
+
writeToSink(sink, record);
|
|
185
|
+
maybePurge();
|
|
13
186
|
}
|
|
14
187
|
//# sourceMappingURL=audit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/audit.ts"],"names":[],"mappings":";;AAQA,oCAQC;AAhBD,yCAAsC;AAEtC;;;;;GAKG;AACH,SAAgB,YAAY,CAAC,KAAa,EAAE,UAAmC,EAAE;IAC/E,IAAI,eAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAChC,IAAI,CAAC,SAAS,iBACZ,KAAK,EACL,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IAChC,OAAO,EACV,CACH,CAAC;AACJ,CAAC","sourcesContent":["import { Logger } from '../../Logger';\n\n/**\n * Audit strutturato degli eventi di sicurezza Accessi.\n *\n * Il Logger redige gia campi come token, password, secret e authorization.\n * Non inserire mai payload completi o segreti nei dettagli.\n */\nexport function auditAccessi(event: string, details: Record<string, unknown> = {}): void {\n new Logger('AccessiAudit').warning(\n JSON.stringify({\n event,\n timestamp: new Date().toISOString(),\n ...details,\n }),\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/audit.ts"],"names":[],"mappings":"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","sourcesContent":["import { Logger } from '../../Logger';\nimport { recordAccessiMetric } from '../observability/metrics';\n\n/** Record di audit normalizzato e gia redatto. */\nexport interface AccessiAuditRecord {\n event: string;\n timestamp: string;\n [key: string]: unknown;\n}\n\n/** Destinazione di un record di audit. */\nexport interface AccessiAuditSink {\n /** Nome usato nelle metriche di errore del sink. */\n name?: string;\n write(record: AccessiAuditRecord): void | Promise<void>;\n}\n\nexport interface AccessiAuditQuery {\n event?: string;\n from?: string;\n to?: string;\n codiceUtente?: number;\n limit?: number;\n offset?: number;\n order?: 'asc' | 'desc';\n}\n\nexport interface AccessiAuditPage {\n records: AccessiAuditRecord[];\n total: number;\n limit: number;\n offset: number;\n}\n\n/** Sink interrogabile dagli endpoint amministrativi e dalla retention. */\nexport interface AccessiAuditQueryableSink extends AccessiAuditSink {\n query(filter?: AccessiAuditQuery): Promise<AccessiAuditPage>;\n purge(before: Date): Promise<number>;\n}\n\n/** Opzioni di audit del modulo. */\nexport interface AccessiAuditOptions {\n /** `false` disattiva i sink ma lascia il log strutturato. Default: abilitati. */\n enabled?: boolean;\n /** Sink aggiuntivi (HTTP, DB, OpenTelemetry). */\n sinks?: AccessiAuditSink[];\n /** Sink interrogabile usato da `/api/accessi/audit`. Default: primo sink queryable. */\n store?: AccessiAuditQueryableSink;\n /** Giorni di retention applicati ai sink queryable. Default: nessuna scadenza automatica. */\n retentionDays?: number;\n /** Chiavi aggiuntive da redigere, oltre a quelle predefinite. */\n redact?: string[];\n /** Scrivi anche sul Logger strutturato. Default: true. */\n logger?: boolean;\n}\n\nconst REDACTED = '[REDACTED]';\nconst TRUNCATED = '[TRUNCATED]';\n\nconst DEFAULT_REDACTED_KEYS = new Set([\n 'token',\n 'accesstoken',\n 'refreshtoken',\n 'idtoken',\n 'bearer',\n 'jwt',\n 'authorization',\n 'cookie',\n 'setcookie',\n 'password',\n 'passwordhash',\n 'hashedpassword',\n 'resettoken',\n 'secret',\n 'clientsecret',\n 'privatekey',\n 'apikey',\n 'csrf',\n 'csrftoken',\n]);\n\nconst MAX_STRING_LENGTH = 512;\nconst MAX_ARRAY_LENGTH = 50;\nconst MAX_OBJECT_KEYS = 100;\nconst MAX_DEPTH = 4;\nconst PURGE_INTERVAL_MS = 60 * 60 * 1000;\n\nlet sinks: AccessiAuditSink[] = [];\nlet redactedKeys = new Set(DEFAULT_REDACTED_KEYS);\nlet retentionDays: number | undefined;\nlet loggerEnabled = true;\nlet lastPurgeAt = 0;\n\nfunction normalizedKey(key: string): string {\n return key.toLowerCase().replace(/[-_\\s]/g, '');\n}\n\n/**\n * Redige ricorsivamente token, segreti, cookie e header sensibili.\n *\n * Limita profondita, lunghezza delle stringhe e numero di elementi per evitare\n * che un payload applicativo finisca per intero nell'audit.\n */\nexport function redactAccessiAuditValue(value: unknown, depth = 0): unknown {\n if (depth > MAX_DEPTH) return TRUNCATED;\n if (value === null || value === undefined) return value;\n if (typeof value === 'string') {\n return value.length > MAX_STRING_LENGTH ? `${value.slice(0, MAX_STRING_LENGTH - 3)}...` : value;\n }\n if (typeof value === 'number' || typeof value === 'boolean') return value;\n if (typeof value === 'bigint') return value.toString();\n if (value instanceof Date) return value.toISOString();\n if (value instanceof Error) {\n return { name: value.name, message: redactAccessiAuditValue(value.message, depth + 1) };\n }\n if (Array.isArray(value)) {\n return value.slice(0, MAX_ARRAY_LENGTH).map((item) => redactAccessiAuditValue(item, depth + 1));\n }\n if (typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(value as Record<string, unknown>).slice(0, MAX_OBJECT_KEYS)) {\n result[key] = redactedKeys.has(normalizedKey(key))\n ? REDACTED\n : redactAccessiAuditValue(entry, depth + 1);\n }\n return result;\n }\n return String(value);\n}\n\nfunction redactDetails(details: Record<string, unknown>): Record<string, unknown> {\n return redactAccessiAuditValue(details) as Record<string, unknown>;\n}\n\nfunction reportSinkError(sink: AccessiAuditSink, error: unknown): void {\n recordAccessiMetric('accessi_audit_sink_errors_total', 1, { sink: sink.name ?? 'unknown' });\n try {\n new Logger('AccessiAudit').error(\n `Audit sink non disponibile${sink.name ? ` (${sink.name})` : ''}: ${error instanceof Error ? error.message : String(error)}`,\n );\n } catch {\n // Il logging non deve mai interrompere l'operazione applicativa.\n }\n}\n\nfunction writeToSink(sink: AccessiAuditSink, record: AccessiAuditRecord): void {\n try {\n const result = sink.write(record);\n if (result && typeof (result as Promise<void>).catch === 'function') {\n (result as Promise<void>).catch((error) => reportSinkError(sink, error));\n }\n } catch (error) {\n reportSinkError(sink, error);\n }\n}\n\nfunction maybePurge(): void {\n if (!retentionDays || retentionDays <= 0) return;\n const now = Date.now();\n if (now - lastPurgeAt < PURGE_INTERVAL_MS) return;\n lastPurgeAt = now;\n const before = new Date(now - retentionDays * 24 * 60 * 60 * 1000);\n for (const sink of sinks) {\n if (typeof (sink as AccessiAuditQueryableSink).purge !== 'function') continue;\n try {\n const result = (sink as AccessiAuditQueryableSink).purge(before);\n if (result && typeof (result as Promise<number>).catch === 'function') {\n (result as Promise<number>).catch((error) => reportSinkError(sink, error));\n }\n } catch (error) {\n reportSinkError(sink, error);\n }\n }\n}\n\n/**\n * Configura i sink di audit del processo.\n *\n * Chiamato da `AccessiModule.forRoot`. Le opzioni sono globali: un solo modulo\n * Accessi per processo e la configurazione consigliata.\n */\nexport function configureAccessiAudit(options: AccessiAuditOptions | undefined): void {\n sinks = [];\n redactedKeys = new Set(DEFAULT_REDACTED_KEYS);\n retentionDays = undefined;\n loggerEnabled = true;\n lastPurgeAt = 0;\n\n for (const key of options?.redact ?? []) redactedKeys.add(normalizedKey(key));\n loggerEnabled = options?.logger !== false;\n if (options?.enabled === false) return;\n\n if (options?.store) sinks.push(options.store);\n for (const sink of options?.sinks ?? []) {\n if (!sinks.includes(sink)) sinks.push(sink);\n }\n const days = Number(options?.retentionDays);\n if (Number.isFinite(days) && days > 0) retentionDays = days;\n}\n\n/** Ripristina la configurazione iniziale (logger attivo, nessun sink). */\nexport function resetAccessiAudit(): void {\n configureAccessiAudit(undefined);\n}\n\n/** Sink interrogabile configurato, usato dagli endpoint amministrativi. */\nexport function getAccessiAuditStore(): AccessiAuditQueryableSink | undefined {\n const configured = sinks.find((sink): sink is AccessiAuditQueryableSink =>\n typeof (sink as AccessiAuditQueryableSink).query === 'function'\n && typeof (sink as AccessiAuditQueryableSink).purge === 'function',\n );\n return configured;\n}\n\n/**\n * Registra un evento di sicurezza.\n *\n * Il record viene redatto prima di raggiungere logger e sink. Un sink guasto\n * non interrompe mai il flusso applicativo.\n */\nexport function auditAccessi(event: string, details: Record<string, unknown> = {}): void {\n const record: AccessiAuditRecord = {\n event,\n timestamp: new Date().toISOString(),\n ...redactDetails(details ?? {}),\n };\n\n if (loggerEnabled) {\n try {\n new Logger('AccessiAudit').warning(JSON.stringify(record));\n } catch {\n // Il logging non deve mai interrompere l'operazione applicativa.\n }\n }\n\n for (const sink of sinks) writeToSink(sink, record);\n maybePurge();\n}\n"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AccessiAuditQueryableSink, AccessiAuditRecord, AccessiAuditSink } from './audit';
|
|
2
|
+
import type { AccessiRedisEvalClient } from '../observability/redisMetrics';
|
|
3
|
+
/** Opzioni del sink in memoria. */
|
|
4
|
+
export interface MemoryAccessiAuditSinkOptions {
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Numero massimo di record conservati. Default: 10000. */
|
|
7
|
+
maxRecords?: number;
|
|
8
|
+
/** Retention in millisecondi. Default: nessuna scadenza. */
|
|
9
|
+
retentionMs?: number;
|
|
10
|
+
/** Orologio iniettabile per i test. */
|
|
11
|
+
now?: () => number;
|
|
12
|
+
}
|
|
13
|
+
export interface MemoryAccessiAuditSink extends AccessiAuditQueryableSink {
|
|
14
|
+
all(): AccessiAuditRecord[];
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sink in memoria con query e retention.
|
|
19
|
+
*
|
|
20
|
+
* Adatto a sviluppo, test e singola istanza. Per piu istanze usare
|
|
21
|
+
* `createRedisAccessiAuditSink` o un sink esterno.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createMemoryAccessiAuditSink(options?: MemoryAccessiAuditSinkOptions): MemoryAccessiAuditSink;
|
|
24
|
+
/** Sink HTTP: inoltra ogni record a un endpoint esterno (SIEM, collector). */
|
|
25
|
+
export interface HttpAccessiAuditSinkOptions {
|
|
26
|
+
name?: string;
|
|
27
|
+
url: string;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare function createHttpAccessiAuditSink(options: HttpAccessiAuditSinkOptions): AccessiAuditSink;
|
|
33
|
+
/**
|
|
34
|
+
* Adattatore per database, OpenTelemetry o qualunque destinazione custom.
|
|
35
|
+
*
|
|
36
|
+
* La callback riceve un record gia redatto e serializzabile.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createCallbackAccessiAuditSink(write: (record: AccessiAuditRecord) => void | Promise<void>, name?: string): AccessiAuditSink;
|
|
39
|
+
export interface RedisAccessiAuditSinkOptions {
|
|
40
|
+
client: AccessiRedisEvalClient;
|
|
41
|
+
name?: string;
|
|
42
|
+
/** Default: `accessi:audit:`. */
|
|
43
|
+
keyPrefix?: string;
|
|
44
|
+
/** Numero massimo di record. Default: 10000. */
|
|
45
|
+
maxRecords?: number;
|
|
46
|
+
/** Retention Redis in millisecondi. Default: 30 giorni. */
|
|
47
|
+
retentionMs?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Sink Redis persistente, condiviso tra istanze e interrogabile.
|
|
51
|
+
*
|
|
52
|
+
* Usa una lista con `LTRIM` per limitare i record e `PEXPIRE` per la retention.
|
|
53
|
+
* Le query leggono la lista e filtrano lato applicazione.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createRedisAccessiAuditSink(options: RedisAccessiAuditSinkOptions): AccessiAuditQueryableSink;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createMemoryAccessiAuditSink = createMemoryAccessiAuditSink;
|
|
13
|
+
exports.createHttpAccessiAuditSink = createHttpAccessiAuditSink;
|
|
14
|
+
exports.createCallbackAccessiAuditSink = createCallbackAccessiAuditSink;
|
|
15
|
+
exports.createRedisAccessiAuditSink = createRedisAccessiAuditSink;
|
|
16
|
+
const redisMetrics_1 = require("../observability/redisMetrics");
|
|
17
|
+
const DEFAULT_LIMIT = 100;
|
|
18
|
+
const MAX_LIMIT = 1000;
|
|
19
|
+
function normalizeQuery(query = {}) {
|
|
20
|
+
const limit = Number.isFinite(query.limit)
|
|
21
|
+
? Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query.limit)))
|
|
22
|
+
: DEFAULT_LIMIT;
|
|
23
|
+
const offset = Number.isFinite(query.offset) ? Math.max(0, Math.trunc(query.offset)) : 0;
|
|
24
|
+
return { limit, offset, order: query.order === 'asc' ? 'asc' : 'desc' };
|
|
25
|
+
}
|
|
26
|
+
function recordTime(record) {
|
|
27
|
+
const time = Date.parse(record.timestamp);
|
|
28
|
+
return Number.isFinite(time) ? time : 0;
|
|
29
|
+
}
|
|
30
|
+
function matches(record, query) {
|
|
31
|
+
if (query.event && record.event !== query.event)
|
|
32
|
+
return false;
|
|
33
|
+
if (query.codiceUtente !== undefined && Number(record.codiceUtente) !== query.codiceUtente)
|
|
34
|
+
return false;
|
|
35
|
+
const time = recordTime(record);
|
|
36
|
+
if (query.from) {
|
|
37
|
+
const from = Date.parse(query.from);
|
|
38
|
+
if (Number.isFinite(from) && time < from)
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (query.to) {
|
|
42
|
+
const to = Date.parse(query.to);
|
|
43
|
+
if (Number.isFinite(to) && time > to)
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
function paginate(records, query) {
|
|
49
|
+
const { limit, offset, order } = normalizeQuery(query);
|
|
50
|
+
const filtered = records.filter((record) => matches(record, query));
|
|
51
|
+
const ordered = order === 'asc' ? filtered : [...filtered].reverse();
|
|
52
|
+
return {
|
|
53
|
+
records: ordered.slice(offset, offset + limit),
|
|
54
|
+
total: filtered.length,
|
|
55
|
+
limit,
|
|
56
|
+
offset,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function cloneRecord(record) {
|
|
60
|
+
return JSON.parse(JSON.stringify(record));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Sink in memoria con query e retention.
|
|
64
|
+
*
|
|
65
|
+
* Adatto a sviluppo, test e singola istanza. Per piu istanze usare
|
|
66
|
+
* `createRedisAccessiAuditSink` o un sink esterno.
|
|
67
|
+
*/
|
|
68
|
+
function createMemoryAccessiAuditSink(options = {}) {
|
|
69
|
+
var _a, _b, _c, _d;
|
|
70
|
+
const maxRecords = Math.max(1, Math.trunc((_a = options.maxRecords) !== null && _a !== void 0 ? _a : 10000));
|
|
71
|
+
const retentionMs = Math.max(0, Number((_b = options.retentionMs) !== null && _b !== void 0 ? _b : 0));
|
|
72
|
+
const now = (_c = options.now) !== null && _c !== void 0 ? _c : Date.now;
|
|
73
|
+
let records = [];
|
|
74
|
+
const purgeExpired = () => {
|
|
75
|
+
if (!retentionMs)
|
|
76
|
+
return;
|
|
77
|
+
const threshold = now() - retentionMs;
|
|
78
|
+
records = records.filter((record) => recordTime(record) >= threshold);
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
name: (_d = options.name) !== null && _d !== void 0 ? _d : 'memory',
|
|
82
|
+
write(record) {
|
|
83
|
+
records.push(cloneRecord(record));
|
|
84
|
+
if (records.length > maxRecords)
|
|
85
|
+
records = records.slice(records.length - maxRecords);
|
|
86
|
+
purgeExpired();
|
|
87
|
+
},
|
|
88
|
+
query() {
|
|
89
|
+
return __awaiter(this, arguments, void 0, function* (query = {}) {
|
|
90
|
+
purgeExpired();
|
|
91
|
+
return paginate(records, query);
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
purge(before) {
|
|
95
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
96
|
+
const threshold = before.getTime();
|
|
97
|
+
const beforeCount = records.length;
|
|
98
|
+
records = records.filter((record) => recordTime(record) >= threshold);
|
|
99
|
+
return beforeCount - records.length;
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
all() {
|
|
103
|
+
return records.map(cloneRecord);
|
|
104
|
+
},
|
|
105
|
+
clear() {
|
|
106
|
+
records = [];
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function createHttpAccessiAuditSink(options) {
|
|
111
|
+
var _a;
|
|
112
|
+
if (!(options === null || options === void 0 ? void 0 : options.url))
|
|
113
|
+
throw new Error('Audit sink HTTP: url obbligatoria.');
|
|
114
|
+
return {
|
|
115
|
+
name: (_a = options.name) !== null && _a !== void 0 ? _a : 'http',
|
|
116
|
+
write(record) {
|
|
117
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
118
|
+
var _a, _b, _c;
|
|
119
|
+
const fetchImpl = (_a = options.fetch) !== null && _a !== void 0 ? _a : globalThis.fetch;
|
|
120
|
+
if (typeof fetchImpl !== 'function') {
|
|
121
|
+
throw new Error('Audit sink HTTP: fetch non disponibile.');
|
|
122
|
+
}
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const timeout = setTimeout(() => controller.abort(), (_b = options.timeoutMs) !== null && _b !== void 0 ? _b : 3000);
|
|
125
|
+
try {
|
|
126
|
+
const response = yield fetchImpl(options.url, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: Object.assign({ 'Content-Type': 'application/json' }, ((_c = options.headers) !== null && _c !== void 0 ? _c : {})),
|
|
129
|
+
body: JSON.stringify(record),
|
|
130
|
+
signal: controller.signal,
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw new Error(`Audit sink HTTP: risposta ${response.status}.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Adattatore per database, OpenTelemetry o qualunque destinazione custom.
|
|
145
|
+
*
|
|
146
|
+
* La callback riceve un record gia redatto e serializzabile.
|
|
147
|
+
*/
|
|
148
|
+
function createCallbackAccessiAuditSink(write, name = 'callback') {
|
|
149
|
+
return { name, write };
|
|
150
|
+
}
|
|
151
|
+
const REDIS_WRITE_SCRIPT = `
|
|
152
|
+
local key = KEYS[1]
|
|
153
|
+
redis.call('RPUSH', key, ARGV[1])
|
|
154
|
+
redis.call('LTRIM', key, -tonumber(ARGV[2]), -1)
|
|
155
|
+
redis.call('PEXPIRE', key, ARGV[3])
|
|
156
|
+
return 1
|
|
157
|
+
`;
|
|
158
|
+
const REDIS_READ_SCRIPT = `return redis.call('LRANGE', KEYS[1], 0, -1)`;
|
|
159
|
+
const REDIS_REWRITE_SCRIPT = `
|
|
160
|
+
local key = KEYS[1]
|
|
161
|
+
redis.call('DEL', key)
|
|
162
|
+
for i = 2, #ARGV do
|
|
163
|
+
redis.call('RPUSH', key, ARGV[i])
|
|
164
|
+
end
|
|
165
|
+
redis.call('PEXPIRE', key, ARGV[1])
|
|
166
|
+
return #ARGV - 1
|
|
167
|
+
`;
|
|
168
|
+
function parseRedisRecords(value) {
|
|
169
|
+
if (!Array.isArray(value))
|
|
170
|
+
return [];
|
|
171
|
+
const records = [];
|
|
172
|
+
for (const entry of value) {
|
|
173
|
+
if (typeof entry !== 'string')
|
|
174
|
+
continue;
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(entry);
|
|
177
|
+
if (parsed && typeof parsed.event === 'string' && typeof parsed.timestamp === 'string') {
|
|
178
|
+
records.push(parsed);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (_a) {
|
|
182
|
+
// Un record corrotto non deve bloccare query ed export.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return records;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Sink Redis persistente, condiviso tra istanze e interrogabile.
|
|
189
|
+
*
|
|
190
|
+
* Usa una lista con `LTRIM` per limitare i record e `PEXPIRE` per la retention.
|
|
191
|
+
* Le query leggono la lista e filtrano lato applicazione.
|
|
192
|
+
*/
|
|
193
|
+
function createRedisAccessiAuditSink(options) {
|
|
194
|
+
var _a, _b, _c, _d;
|
|
195
|
+
if (!(options === null || options === void 0 ? void 0 : options.client) || typeof options.client.eval !== 'function') {
|
|
196
|
+
throw new Error('Audit sink Redis: client.eval non configurato.');
|
|
197
|
+
}
|
|
198
|
+
const client = (0, redisMetrics_1.instrumentAccessiRedisClient)('audit', options.client);
|
|
199
|
+
const key = `${((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:audit:').replace(/:*$/, ':')}events`;
|
|
200
|
+
const maxRecords = Math.max(1, Math.trunc((_b = options.maxRecords) !== null && _b !== void 0 ? _b : 10000));
|
|
201
|
+
const retentionMs = Math.max(1000, Math.trunc((_c = options.retentionMs) !== null && _c !== void 0 ? _c : 30 * 24 * 60 * 60 * 1000));
|
|
202
|
+
const readAll = () => __awaiter(this, void 0, void 0, function* () { return parseRedisRecords(yield client.eval(REDIS_READ_SCRIPT, 1, key)); });
|
|
203
|
+
return {
|
|
204
|
+
name: (_d = options.name) !== null && _d !== void 0 ? _d : 'redis',
|
|
205
|
+
write(record) {
|
|
206
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
207
|
+
yield client.eval(REDIS_WRITE_SCRIPT, 1, key, JSON.stringify(record), maxRecords, retentionMs);
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
query() {
|
|
211
|
+
return __awaiter(this, arguments, void 0, function* (query = {}) {
|
|
212
|
+
return paginate(yield readAll(), query);
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
purge(before) {
|
|
216
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
217
|
+
const records = yield readAll();
|
|
218
|
+
const kept = records.filter((record) => recordTime(record) >= before.getTime());
|
|
219
|
+
const removed = records.length - kept.length;
|
|
220
|
+
if (removed === 0)
|
|
221
|
+
return 0;
|
|
222
|
+
yield client.eval(REDIS_REWRITE_SCRIPT, 1, key, retentionMs, ...kept.map((record) => JSON.stringify(record)));
|
|
223
|
+
return removed;
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=auditSinks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auditSinks.js","sourceRoot":"","sources":["../../../src/accessi-module/security/auditSinks.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AccessiAuditPage,\n AccessiAuditQuery,\n AccessiAuditQueryableSink,\n AccessiAuditRecord,\n AccessiAuditSink,\n} from './audit';\nimport { instrumentAccessiRedisClient } from '../observability/redisMetrics';\nimport type { AccessiRedisEvalClient } from '../observability/redisMetrics';\n\nconst DEFAULT_LIMIT = 100;\nconst MAX_LIMIT = 1_000;\n\nfunction normalizeQuery(query: AccessiAuditQuery = {}): Required<Pick<AccessiAuditQuery, 'limit' | 'offset' | 'order'>> {\n const limit = Number.isFinite(query.limit)\n ? Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query.limit as number)))\n : DEFAULT_LIMIT;\n const offset = Number.isFinite(query.offset) ? Math.max(0, Math.trunc(query.offset as number)) : 0;\n return { limit, offset, order: query.order === 'asc' ? 'asc' : 'desc' };\n}\n\nfunction recordTime(record: AccessiAuditRecord): number {\n const time = Date.parse(record.timestamp);\n return Number.isFinite(time) ? time : 0;\n}\n\nfunction matches(record: AccessiAuditRecord, query: AccessiAuditQuery): boolean {\n if (query.event && record.event !== query.event) return false;\n if (query.codiceUtente !== undefined && Number(record.codiceUtente) !== query.codiceUtente) return false;\n const time = recordTime(record);\n if (query.from) {\n const from = Date.parse(query.from);\n if (Number.isFinite(from) && time < from) return false;\n }\n if (query.to) {\n const to = Date.parse(query.to);\n if (Number.isFinite(to) && time > to) return false;\n }\n return true;\n}\n\nfunction paginate(records: AccessiAuditRecord[], query: AccessiAuditQuery): AccessiAuditPage {\n const { limit, offset, order } = normalizeQuery(query);\n const filtered = records.filter((record) => matches(record, query));\n const ordered = order === 'asc' ? filtered : [...filtered].reverse();\n return {\n records: ordered.slice(offset, offset + limit),\n total: filtered.length,\n limit,\n offset,\n };\n}\n\nfunction cloneRecord(record: AccessiAuditRecord): AccessiAuditRecord {\n return JSON.parse(JSON.stringify(record)) as AccessiAuditRecord;\n}\n\n/** Opzioni del sink in memoria. */\nexport interface MemoryAccessiAuditSinkOptions {\n name?: string;\n /** Numero massimo di record conservati. Default: 10000. */\n maxRecords?: number;\n /** Retention in millisecondi. Default: nessuna scadenza. */\n retentionMs?: number;\n /** Orologio iniettabile per i test. */\n now?: () => number;\n}\n\nexport interface MemoryAccessiAuditSink extends AccessiAuditQueryableSink {\n all(): AccessiAuditRecord[];\n clear(): void;\n}\n\n/**\n * Sink in memoria con query e retention.\n *\n * Adatto a sviluppo, test e singola istanza. Per piu istanze usare\n * `createRedisAccessiAuditSink` o un sink esterno.\n */\nexport function createMemoryAccessiAuditSink(\n options: MemoryAccessiAuditSinkOptions = {},\n): MemoryAccessiAuditSink {\n const maxRecords = Math.max(1, Math.trunc(options.maxRecords ?? 10_000));\n const retentionMs = Math.max(0, Number(options.retentionMs ?? 0));\n const now = options.now ?? Date.now;\n let records: AccessiAuditRecord[] = [];\n\n const purgeExpired = (): void => {\n if (!retentionMs) return;\n const threshold = now() - retentionMs;\n records = records.filter((record) => recordTime(record) >= threshold);\n };\n\n return {\n name: options.name ?? 'memory',\n write(record) {\n records.push(cloneRecord(record));\n if (records.length > maxRecords) records = records.slice(records.length - maxRecords);\n purgeExpired();\n },\n async query(query = {}) {\n purgeExpired();\n return paginate(records, query);\n },\n async purge(before: Date) {\n const threshold = before.getTime();\n const beforeCount = records.length;\n records = records.filter((record) => recordTime(record) >= threshold);\n return beforeCount - records.length;\n },\n all() {\n return records.map(cloneRecord);\n },\n clear() {\n records = [];\n },\n };\n}\n\n/** Sink HTTP: inoltra ogni record a un endpoint esterno (SIEM, collector). */\nexport interface HttpAccessiAuditSinkOptions {\n name?: string;\n url: string;\n headers?: Record<string, string>;\n fetch?: typeof fetch;\n timeoutMs?: number;\n}\n\nexport function createHttpAccessiAuditSink(\n options: HttpAccessiAuditSinkOptions,\n): AccessiAuditSink {\n if (!options?.url) throw new Error('Audit sink HTTP: url obbligatoria.');\n return {\n name: options.name ?? 'http',\n async write(record) {\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new Error('Audit sink HTTP: fetch non disponibile.');\n }\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 3000);\n try {\n const response = await fetchImpl(options.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(options.headers ?? {}),\n },\n body: JSON.stringify(record),\n signal: controller.signal,\n });\n if (!response.ok) {\n throw new Error(`Audit sink HTTP: risposta ${response.status}.`);\n }\n } finally {\n clearTimeout(timeout);\n }\n },\n };\n}\n\n/**\n * Adattatore per database, OpenTelemetry o qualunque destinazione custom.\n *\n * La callback riceve un record gia redatto e serializzabile.\n */\nexport function createCallbackAccessiAuditSink(\n write: (record: AccessiAuditRecord) => void | Promise<void>,\n name = 'callback',\n): AccessiAuditSink {\n return { name, write };\n}\n\nexport interface RedisAccessiAuditSinkOptions {\n client: AccessiRedisEvalClient;\n name?: string;\n /** Default: `accessi:audit:`. */\n keyPrefix?: string;\n /** Numero massimo di record. Default: 10000. */\n maxRecords?: number;\n /** Retention Redis in millisecondi. Default: 30 giorni. */\n retentionMs?: number;\n}\n\nconst REDIS_WRITE_SCRIPT = `\nlocal key = KEYS[1]\nredis.call('RPUSH', key, ARGV[1])\nredis.call('LTRIM', key, -tonumber(ARGV[2]), -1)\nredis.call('PEXPIRE', key, ARGV[3])\nreturn 1\n`;\n\nconst REDIS_READ_SCRIPT = `return redis.call('LRANGE', KEYS[1], 0, -1)`;\n\nconst REDIS_REWRITE_SCRIPT = `\nlocal key = KEYS[1]\nredis.call('DEL', key)\nfor i = 2, #ARGV do\n redis.call('RPUSH', key, ARGV[i])\nend\nredis.call('PEXPIRE', key, ARGV[1])\nreturn #ARGV - 1\n`;\n\nfunction parseRedisRecords(value: unknown): AccessiAuditRecord[] {\n if (!Array.isArray(value)) return [];\n const records: AccessiAuditRecord[] = [];\n for (const entry of value) {\n if (typeof entry !== 'string') continue;\n try {\n const parsed = JSON.parse(entry) as AccessiAuditRecord;\n if (parsed && typeof parsed.event === 'string' && typeof parsed.timestamp === 'string') {\n records.push(parsed);\n }\n } catch {\n // Un record corrotto non deve bloccare query ed export.\n }\n }\n return records;\n}\n\n/**\n * Sink Redis persistente, condiviso tra istanze e interrogabile.\n *\n * Usa una lista con `LTRIM` per limitare i record e `PEXPIRE` per la retention.\n * Le query leggono la lista e filtrano lato applicazione.\n */\nexport function createRedisAccessiAuditSink(\n options: RedisAccessiAuditSinkOptions,\n): AccessiAuditQueryableSink {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Audit sink Redis: client.eval non configurato.');\n }\n const client = instrumentAccessiRedisClient('audit', options.client);\n const key = `${(options.keyPrefix ?? 'accessi:audit:').replace(/:*$/, ':')}events`;\n const maxRecords = Math.max(1, Math.trunc(options.maxRecords ?? 10_000));\n const retentionMs = Math.max(1_000, Math.trunc(options.retentionMs ?? 30 * 24 * 60 * 60 * 1000));\n\n const readAll = async (): Promise<AccessiAuditRecord[]> =>\n parseRedisRecords(await client.eval(REDIS_READ_SCRIPT, 1, key));\n\n return {\n name: options.name ?? 'redis',\n async write(record) {\n await client.eval(REDIS_WRITE_SCRIPT, 1, key, JSON.stringify(record), maxRecords, retentionMs);\n },\n async query(query = {}) {\n return paginate(await readAll(), query);\n },\n async purge(before: Date) {\n const records = await readAll();\n const kept = records.filter((record) => recordTime(record) >= before.getTime());\n const removed = records.length - kept.length;\n if (removed === 0) return 0;\n await client.eval(\n REDIS_REWRITE_SCRIPT,\n 1,\n key,\n retentionMs,\n ...kept.map((record) => JSON.stringify(record)),\n );\n return removed;\n },\n };\n}\n"]}
|
|
@@ -16,6 +16,7 @@ exports.consumeConsoleEntryTicket = consumeConsoleEntryTicket;
|
|
|
16
16
|
exports.clearConsoleEntryTickets = clearConsoleEntryTickets;
|
|
17
17
|
exports.createRedisConsoleEntryStore = createRedisConsoleEntryStore;
|
|
18
18
|
const crypto_1 = require("crypto");
|
|
19
|
+
const redisMetrics_1 = require("../observability/redisMetrics");
|
|
19
20
|
const DEFAULT_TTL_MS = 60000;
|
|
20
21
|
const DEFAULT_MAX_ENTRIES = 5000;
|
|
21
22
|
const TICKET_PATTERN = /^[a-f0-9]{64}$/i;
|
|
@@ -158,6 +159,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
158
159
|
const key = deriveEncryptionKey(options.encryptionKey);
|
|
159
160
|
const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:console-entry:').replace(/:+$/, ':');
|
|
160
161
|
const maxAttempts = Math.max(1, (_b = options.maxIssueAttempts) !== null && _b !== void 0 ? _b : 5);
|
|
162
|
+
const client = (0, redisMetrics_1.instrumentAccessiRedisClient)('console_entry', options.client);
|
|
161
163
|
return {
|
|
162
164
|
issue(record, ttlMs) {
|
|
163
165
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -165,7 +167,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
165
167
|
const value = encryptRecord(record, key);
|
|
166
168
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
167
169
|
const ticket = createTicket();
|
|
168
|
-
const result = yield
|
|
170
|
+
const result = yield client.eval(SET_SCRIPT, 1, `${keyPrefix}${ticket}`, value, safeTtl);
|
|
169
171
|
if (result) {
|
|
170
172
|
return { ticket, expiresInSeconds: Math.ceil(safeTtl / 1000) };
|
|
171
173
|
}
|
|
@@ -178,7 +180,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
178
180
|
if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {
|
|
179
181
|
return null;
|
|
180
182
|
}
|
|
181
|
-
const value = yield
|
|
183
|
+
const value = yield client.eval(CONSUME_SCRIPT, 1, `${keyPrefix}${ticket}`);
|
|
182
184
|
if (typeof value !== 'string')
|
|
183
185
|
return null;
|
|
184
186
|
return decryptRecord(value, key);
|
|
@@ -187,7 +189,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
187
189
|
healthCheck() {
|
|
188
190
|
return __awaiter(this, void 0, void 0, function* () {
|
|
189
191
|
try {
|
|
190
|
-
const result = yield
|
|
192
|
+
const result = yield client.eval("return redis.call('PING')", 0);
|
|
191
193
|
return String(result).toUpperCase() === 'PONG';
|
|
192
194
|
}
|
|
193
195
|
catch (_a) {
|