nestjs-ddd-cli 2.2.0 → 3.2.1
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 +247 -408
- package/ddd.schema.json +111 -0
- package/dist/commands/aggregate-validator.d.ts +9 -0
- package/dist/commands/aggregate-validator.js +953 -0
- package/dist/commands/aggregate-validator.js.map +1 -0
- package/dist/commands/ai-assist.d.ts +8 -0
- package/dist/commands/ai-assist.js +337 -0
- package/dist/commands/ai-assist.js.map +1 -0
- package/dist/commands/api-contracts.d.ts +9 -0
- package/dist/commands/api-contracts.js +1368 -0
- package/dist/commands/api-contracts.js.map +1 -0
- package/dist/commands/api-docs.d.ts +8 -0
- package/dist/commands/api-docs.js +408 -0
- package/dist/commands/api-docs.js.map +1 -0
- package/dist/commands/api-versioning.d.ts +11 -0
- package/dist/commands/api-versioning.js +643 -0
- package/dist/commands/api-versioning.js.map +1 -0
- package/dist/commands/audit-logging.d.ts +9 -0
- package/dist/commands/audit-logging.js +1129 -0
- package/dist/commands/audit-logging.js.map +1 -0
- package/dist/commands/batch-generate.d.ts +10 -0
- package/dist/commands/batch-generate.js +405 -0
- package/dist/commands/batch-generate.js.map +1 -0
- package/dist/commands/caching-strategies.d.ts +9 -0
- package/dist/commands/caching-strategies.js +874 -0
- package/dist/commands/caching-strategies.js.map +1 -0
- package/dist/commands/code-analyzer.d.ts +42 -0
- package/dist/commands/code-analyzer.js +474 -0
- package/dist/commands/code-analyzer.js.map +1 -0
- package/dist/commands/database-seeding.d.ts +6 -0
- package/dist/commands/database-seeding.js +621 -0
- package/dist/commands/database-seeding.js.map +1 -0
- package/dist/commands/db-optimization.d.ts +7 -0
- package/dist/commands/db-optimization.js +687 -0
- package/dist/commands/db-optimization.js.map +1 -0
- package/dist/commands/dependency-graph.d.ts +6 -0
- package/dist/commands/dependency-graph.js +329 -0
- package/dist/commands/dependency-graph.js.map +1 -0
- package/dist/commands/doctor-enhanced.d.ts +22 -0
- package/dist/commands/doctor-enhanced.js +543 -0
- package/dist/commands/doctor-enhanced.js.map +1 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +151 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/env-manager.d.ts +6 -0
- package/dist/commands/env-manager.js +419 -0
- package/dist/commands/env-manager.js.map +1 -0
- package/dist/commands/event-sourcing-full.d.ts +10 -0
- package/dist/commands/event-sourcing-full.js +1107 -0
- package/dist/commands/event-sourcing-full.js.map +1 -0
- package/dist/commands/feature-flags.d.ts +9 -0
- package/dist/commands/feature-flags.js +824 -0
- package/dist/commands/feature-flags.js.map +1 -0
- package/dist/commands/filter-dsl.d.ts +10 -0
- package/dist/commands/filter-dsl.js +1407 -0
- package/dist/commands/filter-dsl.js.map +1 -0
- package/dist/commands/generate-all.js +485 -32
- package/dist/commands/generate-all.js.map +1 -1
- package/dist/commands/generate-deployment.d.ts +8 -0
- package/dist/commands/generate-deployment.js +746 -0
- package/dist/commands/generate-deployment.js.map +1 -0
- package/dist/commands/generate-domain-service.d.ts +14 -0
- package/dist/commands/generate-domain-service.js +796 -0
- package/dist/commands/generate-domain-service.js.map +1 -0
- package/dist/commands/generate-entity.js +82 -24
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/commands/generate-from-schema.d.ts +56 -0
- package/dist/commands/generate-from-schema.js +222 -0
- package/dist/commands/generate-from-schema.js.map +1 -0
- package/dist/commands/generate-orchestrator.d.ts +14 -0
- package/dist/commands/generate-orchestrator.js +887 -0
- package/dist/commands/generate-orchestrator.js.map +1 -0
- package/dist/commands/generate-repository.d.ts +14 -0
- package/dist/commands/generate-repository.js +1019 -0
- package/dist/commands/generate-repository.js.map +1 -0
- package/dist/commands/generate-shared.d.ts +4 -0
- package/dist/commands/generate-shared.js +388 -0
- package/dist/commands/generate-shared.js.map +1 -0
- package/dist/commands/generate-value-object.d.ts +32 -0
- package/dist/commands/generate-value-object.js +700 -0
- package/dist/commands/generate-value-object.js.map +1 -0
- package/dist/commands/graphql-subscriptions.d.ts +6 -0
- package/dist/commands/graphql-subscriptions.js +607 -0
- package/dist/commands/graphql-subscriptions.js.map +1 -0
- package/dist/commands/graphql-types.d.ts +5 -0
- package/dist/commands/graphql-types.js +423 -0
- package/dist/commands/graphql-types.js.map +1 -0
- package/dist/commands/health-probes-advanced.d.ts +6 -0
- package/dist/commands/health-probes-advanced.js +655 -0
- package/dist/commands/health-probes-advanced.js.map +1 -0
- package/dist/commands/i18n-setup.d.ts +10 -0
- package/dist/commands/i18n-setup.js +677 -0
- package/dist/commands/i18n-setup.js.map +1 -0
- package/dist/commands/init-config.d.ts +6 -0
- package/dist/commands/init-config.js +370 -0
- package/dist/commands/init-config.js.map +1 -0
- package/dist/commands/init-project.js +56 -6
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/interactive-scaffold.d.ts +5 -0
- package/dist/commands/interactive-scaffold.js +271 -0
- package/dist/commands/interactive-scaffold.js.map +1 -0
- package/dist/commands/metrics-prometheus.d.ts +6 -0
- package/dist/commands/metrics-prometheus.js +681 -0
- package/dist/commands/metrics-prometheus.js.map +1 -0
- package/dist/commands/migration-engine.d.ts +6 -0
- package/dist/commands/migration-engine.js +446 -0
- package/dist/commands/migration-engine.js.map +1 -0
- package/dist/commands/migration.d.ts +12 -0
- package/dist/commands/migration.js +484 -0
- package/dist/commands/migration.js.map +1 -0
- package/dist/commands/monorepo.d.ts +8 -0
- package/dist/commands/monorepo.js +483 -0
- package/dist/commands/monorepo.js.map +1 -0
- package/dist/commands/multi-database.d.ts +5 -0
- package/dist/commands/multi-database.js +439 -0
- package/dist/commands/multi-database.js.map +1 -0
- package/dist/commands/observability-tracing.d.ts +10 -0
- package/dist/commands/observability-tracing.js +740 -0
- package/dist/commands/observability-tracing.js.map +1 -0
- package/dist/commands/openapi-export.d.ts +8 -0
- package/dist/commands/openapi-export.js +359 -0
- package/dist/commands/openapi-export.js.map +1 -0
- package/dist/commands/perf-analyzer.d.ts +8 -0
- package/dist/commands/perf-analyzer.js +423 -0
- package/dist/commands/perf-analyzer.js.map +1 -0
- package/dist/commands/rate-limiting.d.ts +10 -0
- package/dist/commands/rate-limiting.js +953 -0
- package/dist/commands/rate-limiting.js.map +1 -0
- package/dist/commands/recipe-plugin.d.ts +56 -0
- package/dist/commands/recipe-plugin.js +315 -0
- package/dist/commands/recipe-plugin.js.map +1 -0
- package/dist/commands/recipe.d.ts +6 -0
- package/dist/commands/recipe.js +3941 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.d.ts +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.js +761 -0
- package/dist/commands/recipes/elasticsearch.recipe.js.map +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.d.ts +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.js +889 -0
- package/dist/commands/recipes/event-sourcing.recipe.js.map +1 -0
- package/dist/commands/recipes/index.d.ts +7 -0
- package/dist/commands/recipes/index.js +24 -0
- package/dist/commands/recipes/index.js.map +1 -0
- package/dist/commands/recipes/message-queue.recipe.d.ts +1 -0
- package/dist/commands/recipes/message-queue.recipe.js +706 -0
- package/dist/commands/recipes/message-queue.recipe.js.map +1 -0
- package/dist/commands/recipes/middleware.recipe.d.ts +1 -0
- package/dist/commands/recipes/middleware.recipe.js +383 -0
- package/dist/commands/recipes/middleware.recipe.js.map +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.d.ts +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js +520 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2.recipe.js +472 -0
- package/dist/commands/recipes/oauth2.recipe.js.map +1 -0
- package/dist/commands/recipes/websocket.recipe.d.ts +1 -0
- package/dist/commands/recipes/websocket.recipe.js +453 -0
- package/dist/commands/recipes/websocket.recipe.js.map +1 -0
- package/dist/commands/resilience-patterns.d.ts +13 -0
- package/dist/commands/resilience-patterns.js +1029 -0
- package/dist/commands/resilience-patterns.js.map +1 -0
- package/dist/commands/security-patterns.d.ts +11 -0
- package/dist/commands/security-patterns.js +2233 -0
- package/dist/commands/security-patterns.js.map +1 -0
- package/dist/commands/template-debug.d.ts +27 -0
- package/dist/commands/template-debug.js +388 -0
- package/dist/commands/template-debug.js.map +1 -0
- package/dist/commands/test-factory-full.d.ts +9 -0
- package/dist/commands/test-factory-full.js +1570 -0
- package/dist/commands/test-factory-full.js.map +1 -0
- package/dist/commands/test-scaffold.d.ts +7 -0
- package/dist/commands/test-scaffold.js +621 -0
- package/dist/commands/test-scaffold.js.map +1 -0
- package/dist/index.js +1088 -0
- package/dist/index.js.map +1 -1
- package/dist/templates/ai-context/CLAUDE.md.hbs +158 -0
- package/dist/templates/ai-context/conventions.md.hbs +154 -0
- package/dist/templates/command/create-command.hbs +6 -14
- package/dist/templates/command/delete-command.hbs +19 -0
- package/dist/templates/command/update-command.hbs +24 -0
- package/dist/templates/controller/controller.hbs +64 -17
- package/dist/templates/dto/create-dto.hbs +29 -5
- package/dist/templates/dto/filter-dto.hbs +52 -0
- package/dist/templates/dto/filter-query.dto.hbs +148 -0
- package/dist/templates/dto/paginated-response.dto.hbs +29 -0
- package/dist/templates/dto/pagination-query.dto.hbs +30 -0
- package/dist/templates/dto/response-dto.hbs +38 -0
- package/dist/templates/dto/update-dto.hbs +11 -0
- package/dist/templates/entity/entity.hbs +32 -1
- package/dist/templates/event/domain-event.hbs +33 -7
- package/dist/templates/event/event-handler.hbs +40 -0
- package/dist/templates/exception/base-exceptions.hbs +69 -0
- package/dist/templates/exception/entity-not-found.exception.hbs +7 -0
- package/dist/templates/mapper/mapper.hbs +49 -24
- package/dist/templates/module/module.hbs +34 -10
- package/dist/templates/orm-entity/orm-entity.hbs +63 -12
- package/dist/templates/prisma/prisma-mapper.hbs +71 -0
- package/dist/templates/prisma/prisma-repository.hbs +114 -0
- package/dist/templates/prisma/prisma-schema.hbs +20 -0
- package/dist/templates/prisma/prisma-service.hbs +51 -0
- package/dist/templates/query/get-all.query.hbs +50 -0
- package/dist/templates/query/get-by-id.query.hbs +31 -0
- package/dist/templates/repository/repository.hbs +55 -13
- package/dist/templates/resolver/graphql-input.hbs +54 -0
- package/dist/templates/resolver/graphql-type.hbs +58 -0
- package/dist/templates/resolver/pagination-args.hbs +33 -0
- package/dist/templates/resolver/resolver.hbs +62 -0
- package/dist/templates/shared/prisma-query-builder.util.hbs +189 -0
- package/dist/templates/shared/query-builder.util.hbs +218 -0
- package/dist/templates/test/controller.spec.hbs +124 -0
- package/dist/templates/test/repository.spec.hbs +158 -0
- package/dist/templates/test/usecase.spec.hbs +116 -0
- package/dist/templates/usecase/create-usecase.hbs +19 -7
- package/dist/templates/usecase/delete-usecase.hbs +17 -0
- package/dist/templates/usecase/update-usecase.hbs +31 -0
- package/dist/utils/config.utils.d.ts +45 -0
- package/dist/utils/config.utils.js +211 -0
- package/dist/utils/config.utils.js.map +1 -0
- package/dist/utils/error.utils.d.ts +145 -0
- package/dist/utils/error.utils.js +422 -0
- package/dist/utils/error.utils.js.map +1 -0
- package/dist/utils/field.utils.d.ts +54 -0
- package/dist/utils/field.utils.js +389 -0
- package/dist/utils/field.utils.js.map +1 -0
- package/dist/utils/file.utils.d.ts +19 -8
- package/dist/utils/file.utils.js +135 -4
- package/dist/utils/file.utils.js.map +1 -1
- package/dist/utils/idempotency.utils.d.ts +123 -0
- package/dist/utils/idempotency.utils.js +444 -0
- package/dist/utils/idempotency.utils.js.map +1 -0
- package/dist/utils/naming.utils.js +26 -3
- package/dist/utils/naming.utils.js.map +1 -1
- package/dist/utils/performance.utils.d.ts +37 -0
- package/dist/utils/performance.utils.js +158 -0
- package/dist/utils/performance.utils.js.map +1 -0
- package/dist/utils/relation.utils.d.ts +92 -0
- package/dist/utils/relation.utils.js +388 -0
- package/dist/utils/relation.utils.js.map +1 -0
- package/dist/utils/rollback.utils.d.ts +49 -0
- package/dist/utils/rollback.utils.js +306 -0
- package/dist/utils/rollback.utils.js.map +1 -0
- package/dist/utils/schema.utils.d.ts +123 -0
- package/dist/utils/schema.utils.js +419 -0
- package/dist/utils/schema.utils.js.map +1 -0
- package/dist/utils/security.utils.d.ts +57 -0
- package/dist/utils/security.utils.js +315 -0
- package/dist/utils/security.utils.js.map +1 -0
- package/dist/utils/template-engine.utils.d.ts +80 -0
- package/dist/utils/template-engine.utils.js +463 -0
- package/dist/utils/template-engine.utils.js.map +1 -0
- package/dist/utils/validation-registry.utils.d.ts +160 -0
- package/dist/utils/validation-registry.utils.js +526 -0
- package/dist/utils/validation-registry.utils.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Audit Logging & Compliance Framework Generator
|
|
4
|
+
* Generates comprehensive audit trail infrastructure
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.setupAuditLogging = setupAuditLogging;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
async function setupAuditLogging(basePath, options = {}) {
|
|
48
|
+
console.log(chalk_1.default.bold.blue('\n📝 Setting up Audit Logging Framework\n'));
|
|
49
|
+
const sharedPath = path.join(basePath, 'src/shared/audit');
|
|
50
|
+
if (!fs.existsSync(sharedPath)) {
|
|
51
|
+
fs.mkdirSync(sharedPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
// Generate audit module
|
|
54
|
+
const moduleContent = generateAuditModule();
|
|
55
|
+
fs.writeFileSync(path.join(sharedPath, 'audit.module.ts'), moduleContent);
|
|
56
|
+
console.log(chalk_1.default.green(` ✓ Created audit module`));
|
|
57
|
+
// Generate audit service
|
|
58
|
+
const serviceContent = generateAuditService(options);
|
|
59
|
+
fs.writeFileSync(path.join(sharedPath, 'audit.service.ts'), serviceContent);
|
|
60
|
+
console.log(chalk_1.default.green(` ✓ Created audit service`));
|
|
61
|
+
// Generate audit interceptor
|
|
62
|
+
const interceptorContent = generateAuditInterceptor();
|
|
63
|
+
fs.writeFileSync(path.join(sharedPath, 'audit.interceptor.ts'), interceptorContent);
|
|
64
|
+
console.log(chalk_1.default.green(` ✓ Created audit interceptor`));
|
|
65
|
+
// Generate audit entity
|
|
66
|
+
const entityContent = generateAuditEntity();
|
|
67
|
+
fs.writeFileSync(path.join(sharedPath, 'audit-log.entity.ts'), entityContent);
|
|
68
|
+
console.log(chalk_1.default.green(` ✓ Created audit log entity`));
|
|
69
|
+
// Generate audit decorators
|
|
70
|
+
const decoratorContent = generateAuditDecorators();
|
|
71
|
+
fs.writeFileSync(path.join(sharedPath, 'audit.decorators.ts'), decoratorContent);
|
|
72
|
+
console.log(chalk_1.default.green(` ✓ Created audit decorators`));
|
|
73
|
+
// Generate compliance reporter
|
|
74
|
+
const reporterContent = generateComplianceReporter();
|
|
75
|
+
fs.writeFileSync(path.join(sharedPath, 'compliance.reporter.ts'), reporterContent);
|
|
76
|
+
console.log(chalk_1.default.green(` ✓ Created compliance reporter`));
|
|
77
|
+
console.log(chalk_1.default.bold.green('\n✅ Audit logging framework ready!\n'));
|
|
78
|
+
}
|
|
79
|
+
function generateAuditModule() {
|
|
80
|
+
return `import { Module, Global, DynamicModule } from '@nestjs/common';
|
|
81
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
82
|
+
import { APP_INTERCEPTOR } from '@nestjs/core';
|
|
83
|
+
import { AuditService } from './audit.service';
|
|
84
|
+
import { AuditInterceptor } from './audit.interceptor';
|
|
85
|
+
import { AuditLog } from './audit-log.entity';
|
|
86
|
+
import { ComplianceReporter } from './compliance.reporter';
|
|
87
|
+
|
|
88
|
+
export interface AuditModuleOptions {
|
|
89
|
+
storage: 'database' | 'file' | 'elasticsearch';
|
|
90
|
+
retentionDays?: number;
|
|
91
|
+
excludePaths?: string[];
|
|
92
|
+
excludeMethods?: string[];
|
|
93
|
+
sensitiveFields?: string[];
|
|
94
|
+
enableCompression?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Global()
|
|
98
|
+
@Module({})
|
|
99
|
+
export class AuditModule {
|
|
100
|
+
static forRoot(options: AuditModuleOptions): DynamicModule {
|
|
101
|
+
return {
|
|
102
|
+
module: AuditModule,
|
|
103
|
+
imports: [
|
|
104
|
+
TypeOrmModule.forFeature([AuditLog]),
|
|
105
|
+
],
|
|
106
|
+
providers: [
|
|
107
|
+
{
|
|
108
|
+
provide: 'AUDIT_OPTIONS',
|
|
109
|
+
useValue: options,
|
|
110
|
+
},
|
|
111
|
+
AuditService,
|
|
112
|
+
ComplianceReporter,
|
|
113
|
+
{
|
|
114
|
+
provide: APP_INTERCEPTOR,
|
|
115
|
+
useClass: AuditInterceptor,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
exports: [AuditService, ComplianceReporter],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
function generateAuditService(options) {
|
|
125
|
+
return `import { Injectable, Inject, Logger } from '@nestjs/common';
|
|
126
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
127
|
+
import { Repository, Between, Like } from 'typeorm';
|
|
128
|
+
import { AuditLog, AuditAction, AuditCategory } from './audit-log.entity';
|
|
129
|
+
|
|
130
|
+
export interface AuditEntry {
|
|
131
|
+
action: AuditAction;
|
|
132
|
+
category: AuditCategory;
|
|
133
|
+
userId?: string;
|
|
134
|
+
resourceType: string;
|
|
135
|
+
resourceId?: string;
|
|
136
|
+
description: string;
|
|
137
|
+
oldValue?: any;
|
|
138
|
+
newValue?: any;
|
|
139
|
+
metadata?: Record<string, any>;
|
|
140
|
+
ipAddress?: string;
|
|
141
|
+
userAgent?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface AuditQuery {
|
|
145
|
+
userId?: string;
|
|
146
|
+
action?: AuditAction;
|
|
147
|
+
category?: AuditCategory;
|
|
148
|
+
resourceType?: string;
|
|
149
|
+
resourceId?: string;
|
|
150
|
+
startDate?: Date;
|
|
151
|
+
endDate?: Date;
|
|
152
|
+
page?: number;
|
|
153
|
+
pageSize?: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Injectable()
|
|
157
|
+
export class AuditService {
|
|
158
|
+
private readonly logger = new Logger(AuditService.name);
|
|
159
|
+
|
|
160
|
+
constructor(
|
|
161
|
+
@InjectRepository(AuditLog)
|
|
162
|
+
private readonly auditRepository: Repository<AuditLog>,
|
|
163
|
+
@Inject('AUDIT_OPTIONS') private readonly options: any,
|
|
164
|
+
) {}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Log an audit entry
|
|
168
|
+
*/
|
|
169
|
+
async log(entry: AuditEntry): Promise<AuditLog> {
|
|
170
|
+
const auditLog = this.auditRepository.create({
|
|
171
|
+
...entry,
|
|
172
|
+
oldValue: this.sanitize(entry.oldValue),
|
|
173
|
+
newValue: this.sanitize(entry.newValue),
|
|
174
|
+
timestamp: new Date(),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const saved = await this.auditRepository.save(auditLog);
|
|
178
|
+
this.logger.debug(\`Audit log created: \${entry.action} on \${entry.resourceType}\`);
|
|
179
|
+
|
|
180
|
+
return saved;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Log a create action
|
|
185
|
+
*/
|
|
186
|
+
async logCreate(params: {
|
|
187
|
+
userId?: string;
|
|
188
|
+
resourceType: string;
|
|
189
|
+
resourceId: string;
|
|
190
|
+
newValue: any;
|
|
191
|
+
metadata?: Record<string, any>;
|
|
192
|
+
}): Promise<AuditLog> {
|
|
193
|
+
return this.log({
|
|
194
|
+
action: AuditAction.CREATE,
|
|
195
|
+
category: AuditCategory.DATA_CHANGE,
|
|
196
|
+
description: \`Created \${params.resourceType} with ID \${params.resourceId}\`,
|
|
197
|
+
...params,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Log an update action
|
|
203
|
+
*/
|
|
204
|
+
async logUpdate(params: {
|
|
205
|
+
userId?: string;
|
|
206
|
+
resourceType: string;
|
|
207
|
+
resourceId: string;
|
|
208
|
+
oldValue: any;
|
|
209
|
+
newValue: any;
|
|
210
|
+
metadata?: Record<string, any>;
|
|
211
|
+
}): Promise<AuditLog> {
|
|
212
|
+
const changes = this.computeChanges(params.oldValue, params.newValue);
|
|
213
|
+
|
|
214
|
+
return this.log({
|
|
215
|
+
action: AuditAction.UPDATE,
|
|
216
|
+
category: AuditCategory.DATA_CHANGE,
|
|
217
|
+
description: \`Updated \${params.resourceType} with ID \${params.resourceId}\`,
|
|
218
|
+
metadata: { ...params.metadata, changes },
|
|
219
|
+
...params,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Log a delete action
|
|
225
|
+
*/
|
|
226
|
+
async logDelete(params: {
|
|
227
|
+
userId?: string;
|
|
228
|
+
resourceType: string;
|
|
229
|
+
resourceId: string;
|
|
230
|
+
oldValue?: any;
|
|
231
|
+
metadata?: Record<string, any>;
|
|
232
|
+
}): Promise<AuditLog> {
|
|
233
|
+
return this.log({
|
|
234
|
+
action: AuditAction.DELETE,
|
|
235
|
+
category: AuditCategory.DATA_CHANGE,
|
|
236
|
+
description: \`Deleted \${params.resourceType} with ID \${params.resourceId}\`,
|
|
237
|
+
...params,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Log an access action
|
|
243
|
+
*/
|
|
244
|
+
async logAccess(params: {
|
|
245
|
+
userId?: string;
|
|
246
|
+
resourceType: string;
|
|
247
|
+
resourceId?: string;
|
|
248
|
+
metadata?: Record<string, any>;
|
|
249
|
+
}): Promise<AuditLog> {
|
|
250
|
+
return this.log({
|
|
251
|
+
action: AuditAction.READ,
|
|
252
|
+
category: AuditCategory.DATA_ACCESS,
|
|
253
|
+
description: \`Accessed \${params.resourceType}\${params.resourceId ? \` with ID \${params.resourceId}\` : ''}\`,
|
|
254
|
+
...params,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Log authentication event
|
|
260
|
+
*/
|
|
261
|
+
async logAuth(params: {
|
|
262
|
+
action: 'login' | 'logout' | 'failed_login' | 'password_change';
|
|
263
|
+
userId?: string;
|
|
264
|
+
ipAddress?: string;
|
|
265
|
+
userAgent?: string;
|
|
266
|
+
metadata?: Record<string, any>;
|
|
267
|
+
}): Promise<AuditLog> {
|
|
268
|
+
const actionMap: Record<string, AuditAction> = {
|
|
269
|
+
login: AuditAction.LOGIN,
|
|
270
|
+
logout: AuditAction.LOGOUT,
|
|
271
|
+
failed_login: AuditAction.LOGIN,
|
|
272
|
+
password_change: AuditAction.UPDATE,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
return this.log({
|
|
276
|
+
action: actionMap[params.action],
|
|
277
|
+
category: AuditCategory.AUTHENTICATION,
|
|
278
|
+
resourceType: 'user',
|
|
279
|
+
resourceId: params.userId,
|
|
280
|
+
description: \`User \${params.action.replace('_', ' ')}\`,
|
|
281
|
+
ipAddress: params.ipAddress,
|
|
282
|
+
userAgent: params.userAgent,
|
|
283
|
+
metadata: {
|
|
284
|
+
...params.metadata,
|
|
285
|
+
success: params.action !== 'failed_login',
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Query audit logs
|
|
292
|
+
*/
|
|
293
|
+
async query(query: AuditQuery): Promise<{ items: AuditLog[]; total: number }> {
|
|
294
|
+
const {
|
|
295
|
+
page = 1,
|
|
296
|
+
pageSize = 50,
|
|
297
|
+
startDate,
|
|
298
|
+
endDate,
|
|
299
|
+
...filters
|
|
300
|
+
} = query;
|
|
301
|
+
|
|
302
|
+
const where: any = {};
|
|
303
|
+
|
|
304
|
+
if (filters.userId) where.userId = filters.userId;
|
|
305
|
+
if (filters.action) where.action = filters.action;
|
|
306
|
+
if (filters.category) where.category = filters.category;
|
|
307
|
+
if (filters.resourceType) where.resourceType = filters.resourceType;
|
|
308
|
+
if (filters.resourceId) where.resourceId = filters.resourceId;
|
|
309
|
+
|
|
310
|
+
if (startDate && endDate) {
|
|
311
|
+
where.timestamp = Between(startDate, endDate);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const [items, total] = await this.auditRepository.findAndCount({
|
|
315
|
+
where,
|
|
316
|
+
order: { timestamp: 'DESC' },
|
|
317
|
+
skip: (page - 1) * pageSize,
|
|
318
|
+
take: pageSize,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return { items, total };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get audit trail for a resource
|
|
326
|
+
*/
|
|
327
|
+
async getResourceAuditTrail(
|
|
328
|
+
resourceType: string,
|
|
329
|
+
resourceId: string,
|
|
330
|
+
): Promise<AuditLog[]> {
|
|
331
|
+
return this.auditRepository.find({
|
|
332
|
+
where: { resourceType, resourceId },
|
|
333
|
+
order: { timestamp: 'DESC' },
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Get user activity log
|
|
339
|
+
*/
|
|
340
|
+
async getUserActivity(userId: string, days: number = 30): Promise<AuditLog[]> {
|
|
341
|
+
const startDate = new Date();
|
|
342
|
+
startDate.setDate(startDate.getDate() - days);
|
|
343
|
+
|
|
344
|
+
return this.auditRepository.find({
|
|
345
|
+
where: {
|
|
346
|
+
userId,
|
|
347
|
+
timestamp: Between(startDate, new Date()),
|
|
348
|
+
},
|
|
349
|
+
order: { timestamp: 'DESC' },
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Sanitize sensitive data - removes/masks PII and secrets
|
|
355
|
+
* OWASP A09:2021 - Security Logging and Monitoring Failures
|
|
356
|
+
*/
|
|
357
|
+
private sanitize(data: any, depth = 0): any {
|
|
358
|
+
// Prevent infinite recursion
|
|
359
|
+
if (depth > 10 || !data) return data;
|
|
360
|
+
|
|
361
|
+
// Handle strings with patterns
|
|
362
|
+
if (typeof data === 'string') {
|
|
363
|
+
return this.sanitizeString(data);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Handle arrays
|
|
367
|
+
if (Array.isArray(data)) {
|
|
368
|
+
return data.map(item => this.sanitize(item, depth + 1));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Handle objects
|
|
372
|
+
if (typeof data === 'object' && data !== null) {
|
|
373
|
+
const sanitized: Record<string, any> = {};
|
|
374
|
+
|
|
375
|
+
// Default sensitive field names (case-insensitive matching)
|
|
376
|
+
const sensitiveFieldPatterns = this.options.sensitiveFields || [
|
|
377
|
+
/password/i,
|
|
378
|
+
/token/i,
|
|
379
|
+
/secret/i,
|
|
380
|
+
/api[_-]?key/i,
|
|
381
|
+
/auth/i,
|
|
382
|
+
/credential/i,
|
|
383
|
+
/credit[_-]?card/i,
|
|
384
|
+
/card[_-]?number/i,
|
|
385
|
+
/cvv/i,
|
|
386
|
+
/cvc/i,
|
|
387
|
+
/ssn/i,
|
|
388
|
+
/social[_-]?security/i,
|
|
389
|
+
/tax[_-]?id/i,
|
|
390
|
+
/passport/i,
|
|
391
|
+
/license/i,
|
|
392
|
+
/pin/i,
|
|
393
|
+
/private[_-]?key/i,
|
|
394
|
+
/access[_-]?token/i,
|
|
395
|
+
/refresh[_-]?token/i,
|
|
396
|
+
/bearer/i,
|
|
397
|
+
/authorization/i,
|
|
398
|
+
/cookie/i,
|
|
399
|
+
/session/i,
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
for (const [key, value] of Object.entries(data)) {
|
|
403
|
+
// Check if field name matches sensitive patterns
|
|
404
|
+
const isSensitive = sensitiveFieldPatterns.some(pattern =>
|
|
405
|
+
typeof pattern === 'string'
|
|
406
|
+
? key.toLowerCase().includes(pattern.toLowerCase())
|
|
407
|
+
: pattern.test(key)
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
if (isSensitive && value) {
|
|
411
|
+
sanitized[key] = '[REDACTED]';
|
|
412
|
+
} else {
|
|
413
|
+
sanitized[key] = this.sanitize(value, depth + 1);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return sanitized;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return data;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Sanitize string values that may contain sensitive patterns
|
|
425
|
+
*/
|
|
426
|
+
private sanitizeString(value: string): string {
|
|
427
|
+
if (!value || typeof value !== 'string') return value;
|
|
428
|
+
|
|
429
|
+
let sanitized = value;
|
|
430
|
+
|
|
431
|
+
// Mask credit card numbers (13-19 digits)
|
|
432
|
+
sanitized = sanitized.replace(/\\b(\\d{4})[\\s-]?(\\d{4,6})[\\s-]?(\\d{4,5})[\\s-]?(\\d{4})\\b/g, '$1****$4');
|
|
433
|
+
|
|
434
|
+
// Mask email addresses
|
|
435
|
+
sanitized = sanitized.replace(/([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})/g, (match, user, domain) => {
|
|
436
|
+
const maskedUser = user.substring(0, 2) + '***';
|
|
437
|
+
return \`\${maskedUser}@\${domain}\`;
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Mask phone numbers (various formats)
|
|
441
|
+
sanitized = sanitized.replace(/\\b(\\+?\\d{1,3}[-.\\s]?)?(\\(?\\d{3}\\)?[-.\\s]?)(\\d{3})[-.\\s]?(\\d{4})\\b/g, '$1$2***-$4');
|
|
442
|
+
|
|
443
|
+
// Mask SSN-like patterns
|
|
444
|
+
sanitized = sanitized.replace(/\\b(\\d{3})[-.]?(\\d{2})[-.]?(\\d{4})\\b/g, '***-**-$3');
|
|
445
|
+
|
|
446
|
+
// Mask bearer tokens
|
|
447
|
+
sanitized = sanitized.replace(/Bearer\\s+[a-zA-Z0-9._-]+/gi, 'Bearer [REDACTED]');
|
|
448
|
+
|
|
449
|
+
// Mask API keys (common formats)
|
|
450
|
+
sanitized = sanitized.replace(/([a-z]{2,}_)?[a-zA-Z0-9]{20,}/g, (match) => {
|
|
451
|
+
if (match.length > 10) {
|
|
452
|
+
return match.substring(0, 4) + '****' + match.substring(match.length - 4);
|
|
453
|
+
}
|
|
454
|
+
return match;
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
return sanitized;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Compute changes between old and new values
|
|
462
|
+
*/
|
|
463
|
+
private computeChanges(oldValue: any, newValue: any): Record<string, { from: any; to: any }> {
|
|
464
|
+
const changes: Record<string, { from: any; to: any }> = {};
|
|
465
|
+
|
|
466
|
+
if (!oldValue || !newValue) return changes;
|
|
467
|
+
|
|
468
|
+
const allKeys = new Set([
|
|
469
|
+
...Object.keys(oldValue),
|
|
470
|
+
...Object.keys(newValue),
|
|
471
|
+
]);
|
|
472
|
+
|
|
473
|
+
for (const key of allKeys) {
|
|
474
|
+
if (JSON.stringify(oldValue[key]) !== JSON.stringify(newValue[key])) {
|
|
475
|
+
changes[key] = {
|
|
476
|
+
from: oldValue[key],
|
|
477
|
+
to: newValue[key],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return changes;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Archive old audit logs
|
|
487
|
+
*/
|
|
488
|
+
async archive(olderThan: Date): Promise<number> {
|
|
489
|
+
const result = await this.auditRepository
|
|
490
|
+
.createQueryBuilder()
|
|
491
|
+
.update()
|
|
492
|
+
.set({ archived: true })
|
|
493
|
+
.where('timestamp < :date', { date: olderThan })
|
|
494
|
+
.andWhere('archived = :archived', { archived: false })
|
|
495
|
+
.execute();
|
|
496
|
+
|
|
497
|
+
return result.affected || 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Cleanup old audit logs
|
|
502
|
+
*/
|
|
503
|
+
async cleanup(retentionDays?: number): Promise<number> {
|
|
504
|
+
const days = retentionDays || this.options.retentionDays || 365;
|
|
505
|
+
const cutoffDate = new Date();
|
|
506
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
507
|
+
|
|
508
|
+
const result = await this.auditRepository.delete({
|
|
509
|
+
timestamp: Between(new Date(0), cutoffDate),
|
|
510
|
+
archived: true,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return result.affected || 0;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
`;
|
|
517
|
+
}
|
|
518
|
+
function generateAuditInterceptor() {
|
|
519
|
+
return `import {
|
|
520
|
+
Injectable,
|
|
521
|
+
NestInterceptor,
|
|
522
|
+
ExecutionContext,
|
|
523
|
+
CallHandler,
|
|
524
|
+
Inject,
|
|
525
|
+
} from '@nestjs/common';
|
|
526
|
+
import { Observable } from 'rxjs';
|
|
527
|
+
import { tap } from 'rxjs/operators';
|
|
528
|
+
import { Reflector } from '@nestjs/core';
|
|
529
|
+
import { AuditService } from './audit.service';
|
|
530
|
+
import { AuditAction, AuditCategory } from './audit-log.entity';
|
|
531
|
+
|
|
532
|
+
@Injectable()
|
|
533
|
+
export class AuditInterceptor implements NestInterceptor {
|
|
534
|
+
constructor(
|
|
535
|
+
private readonly auditService: AuditService,
|
|
536
|
+
private readonly reflector: Reflector,
|
|
537
|
+
@Inject('AUDIT_OPTIONS') private readonly options: any,
|
|
538
|
+
) {}
|
|
539
|
+
|
|
540
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
541
|
+
// Check if auditing is disabled for this handler
|
|
542
|
+
const skipAudit = this.reflector.get<boolean>('skipAudit', context.getHandler());
|
|
543
|
+
if (skipAudit) {
|
|
544
|
+
return next.handle();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Check if path is excluded
|
|
548
|
+
const request = context.switchToHttp().getRequest();
|
|
549
|
+
if (this.shouldExclude(request)) {
|
|
550
|
+
return next.handle();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Get audit metadata
|
|
554
|
+
const auditMeta = this.reflector.get<AuditMetadata>('audit', context.getHandler());
|
|
555
|
+
const startTime = Date.now();
|
|
556
|
+
|
|
557
|
+
return next.handle().pipe(
|
|
558
|
+
tap({
|
|
559
|
+
next: (data) => {
|
|
560
|
+
this.logRequest(context, request, data, startTime, auditMeta);
|
|
561
|
+
},
|
|
562
|
+
error: (error) => {
|
|
563
|
+
this.logError(context, request, error, startTime, auditMeta);
|
|
564
|
+
},
|
|
565
|
+
}),
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private shouldExclude(request: any): boolean {
|
|
570
|
+
const excludePaths = this.options.excludePaths || ['/health', '/metrics'];
|
|
571
|
+
const excludeMethods = this.options.excludeMethods || ['OPTIONS'];
|
|
572
|
+
|
|
573
|
+
if (excludeMethods.includes(request.method)) {
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
for (const path of excludePaths) {
|
|
578
|
+
if (request.path.startsWith(path)) {
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private async logRequest(
|
|
587
|
+
context: ExecutionContext,
|
|
588
|
+
request: any,
|
|
589
|
+
response: any,
|
|
590
|
+
startTime: number,
|
|
591
|
+
auditMeta?: AuditMetadata,
|
|
592
|
+
): Promise<void> {
|
|
593
|
+
const action = this.getActionFromMethod(request.method);
|
|
594
|
+
const duration = Date.now() - startTime;
|
|
595
|
+
|
|
596
|
+
await this.auditService.log({
|
|
597
|
+
action,
|
|
598
|
+
category: auditMeta?.category || AuditCategory.API_CALL,
|
|
599
|
+
userId: request.user?.id,
|
|
600
|
+
resourceType: auditMeta?.resourceType || this.extractResourceType(request.path),
|
|
601
|
+
resourceId: auditMeta?.resourceId || request.params?.id,
|
|
602
|
+
description: auditMeta?.description || \`\${request.method} \${request.path}\`,
|
|
603
|
+
metadata: {
|
|
604
|
+
method: request.method,
|
|
605
|
+
path: request.path,
|
|
606
|
+
query: request.query,
|
|
607
|
+
duration,
|
|
608
|
+
statusCode: context.switchToHttp().getResponse().statusCode,
|
|
609
|
+
},
|
|
610
|
+
ipAddress: request.ip,
|
|
611
|
+
userAgent: request.headers['user-agent'],
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private async logError(
|
|
616
|
+
context: ExecutionContext,
|
|
617
|
+
request: any,
|
|
618
|
+
error: Error,
|
|
619
|
+
startTime: number,
|
|
620
|
+
auditMeta?: AuditMetadata,
|
|
621
|
+
): Promise<void> {
|
|
622
|
+
const duration = Date.now() - startTime;
|
|
623
|
+
|
|
624
|
+
await this.auditService.log({
|
|
625
|
+
action: AuditAction.ERROR,
|
|
626
|
+
category: AuditCategory.ERROR,
|
|
627
|
+
userId: request.user?.id,
|
|
628
|
+
resourceType: auditMeta?.resourceType || this.extractResourceType(request.path),
|
|
629
|
+
resourceId: request.params?.id,
|
|
630
|
+
description: \`Error: \${error.message}\`,
|
|
631
|
+
metadata: {
|
|
632
|
+
method: request.method,
|
|
633
|
+
path: request.path,
|
|
634
|
+
duration,
|
|
635
|
+
errorName: error.name,
|
|
636
|
+
errorStack: error.stack,
|
|
637
|
+
},
|
|
638
|
+
ipAddress: request.ip,
|
|
639
|
+
userAgent: request.headers['user-agent'],
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private getActionFromMethod(method: string): AuditAction {
|
|
644
|
+
const actionMap: Record<string, AuditAction> = {
|
|
645
|
+
GET: AuditAction.READ,
|
|
646
|
+
POST: AuditAction.CREATE,
|
|
647
|
+
PUT: AuditAction.UPDATE,
|
|
648
|
+
PATCH: AuditAction.UPDATE,
|
|
649
|
+
DELETE: AuditAction.DELETE,
|
|
650
|
+
};
|
|
651
|
+
return actionMap[method] || AuditAction.OTHER;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private extractResourceType(path: string): string {
|
|
655
|
+
const parts = path.split('/').filter(Boolean);
|
|
656
|
+
// Remove version prefix and get resource name
|
|
657
|
+
return parts.find(p => !p.startsWith('v') && !p.match(/^\\d+$/)) || 'unknown';
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
interface AuditMetadata {
|
|
662
|
+
action?: AuditAction;
|
|
663
|
+
category?: AuditCategory;
|
|
664
|
+
resourceType?: string;
|
|
665
|
+
resourceId?: string;
|
|
666
|
+
description?: string;
|
|
667
|
+
}
|
|
668
|
+
`;
|
|
669
|
+
}
|
|
670
|
+
function generateAuditEntity() {
|
|
671
|
+
return `import {
|
|
672
|
+
Entity,
|
|
673
|
+
PrimaryGeneratedColumn,
|
|
674
|
+
Column,
|
|
675
|
+
CreateDateColumn,
|
|
676
|
+
Index,
|
|
677
|
+
} from 'typeorm';
|
|
678
|
+
|
|
679
|
+
export enum AuditAction {
|
|
680
|
+
CREATE = 'CREATE',
|
|
681
|
+
READ = 'READ',
|
|
682
|
+
UPDATE = 'UPDATE',
|
|
683
|
+
DELETE = 'DELETE',
|
|
684
|
+
LOGIN = 'LOGIN',
|
|
685
|
+
LOGOUT = 'LOGOUT',
|
|
686
|
+
EXPORT = 'EXPORT',
|
|
687
|
+
IMPORT = 'IMPORT',
|
|
688
|
+
ERROR = 'ERROR',
|
|
689
|
+
OTHER = 'OTHER',
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export enum AuditCategory {
|
|
693
|
+
AUTHENTICATION = 'AUTHENTICATION',
|
|
694
|
+
AUTHORIZATION = 'AUTHORIZATION',
|
|
695
|
+
DATA_CHANGE = 'DATA_CHANGE',
|
|
696
|
+
DATA_ACCESS = 'DATA_ACCESS',
|
|
697
|
+
CONFIGURATION = 'CONFIGURATION',
|
|
698
|
+
SECURITY = 'SECURITY',
|
|
699
|
+
API_CALL = 'API_CALL',
|
|
700
|
+
ERROR = 'ERROR',
|
|
701
|
+
COMPLIANCE = 'COMPLIANCE',
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
@Entity('audit_logs')
|
|
705
|
+
@Index(['userId', 'timestamp'])
|
|
706
|
+
@Index(['resourceType', 'resourceId'])
|
|
707
|
+
@Index(['action', 'timestamp'])
|
|
708
|
+
@Index(['category', 'timestamp'])
|
|
709
|
+
export class AuditLog {
|
|
710
|
+
@PrimaryGeneratedColumn('uuid')
|
|
711
|
+
id: string;
|
|
712
|
+
|
|
713
|
+
@Column({ type: 'enum', enum: AuditAction })
|
|
714
|
+
action: AuditAction;
|
|
715
|
+
|
|
716
|
+
@Column({ type: 'enum', enum: AuditCategory })
|
|
717
|
+
category: AuditCategory;
|
|
718
|
+
|
|
719
|
+
@Column({ nullable: true })
|
|
720
|
+
@Index()
|
|
721
|
+
userId: string;
|
|
722
|
+
|
|
723
|
+
@Column()
|
|
724
|
+
resourceType: string;
|
|
725
|
+
|
|
726
|
+
@Column({ nullable: true })
|
|
727
|
+
resourceId: string;
|
|
728
|
+
|
|
729
|
+
@Column('text')
|
|
730
|
+
description: string;
|
|
731
|
+
|
|
732
|
+
@Column({ type: 'jsonb', nullable: true })
|
|
733
|
+
oldValue: any;
|
|
734
|
+
|
|
735
|
+
@Column({ type: 'jsonb', nullable: true })
|
|
736
|
+
newValue: any;
|
|
737
|
+
|
|
738
|
+
@Column({ type: 'jsonb', nullable: true })
|
|
739
|
+
metadata: Record<string, any>;
|
|
740
|
+
|
|
741
|
+
@Column({ nullable: true })
|
|
742
|
+
ipAddress: string;
|
|
743
|
+
|
|
744
|
+
@Column({ nullable: true })
|
|
745
|
+
userAgent: string;
|
|
746
|
+
|
|
747
|
+
@CreateDateColumn()
|
|
748
|
+
@Index()
|
|
749
|
+
timestamp: Date;
|
|
750
|
+
|
|
751
|
+
@Column({ default: false })
|
|
752
|
+
archived: boolean;
|
|
753
|
+
}
|
|
754
|
+
`;
|
|
755
|
+
}
|
|
756
|
+
function generateAuditDecorators() {
|
|
757
|
+
return `import { SetMetadata, applyDecorators } from '@nestjs/common';
|
|
758
|
+
import { AuditAction, AuditCategory } from './audit-log.entity';
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Skip auditing for a handler
|
|
762
|
+
*/
|
|
763
|
+
export function SkipAudit() {
|
|
764
|
+
return SetMetadata('skipAudit', true);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Custom audit metadata
|
|
769
|
+
*/
|
|
770
|
+
export function Audit(options: {
|
|
771
|
+
action?: AuditAction;
|
|
772
|
+
category?: AuditCategory;
|
|
773
|
+
resourceType?: string;
|
|
774
|
+
description?: string;
|
|
775
|
+
}) {
|
|
776
|
+
return SetMetadata('audit', options);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Audit as data access
|
|
781
|
+
*/
|
|
782
|
+
export function AuditDataAccess(resourceType: string) {
|
|
783
|
+
return Audit({
|
|
784
|
+
action: AuditAction.READ,
|
|
785
|
+
category: AuditCategory.DATA_ACCESS,
|
|
786
|
+
resourceType,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Audit as data change
|
|
792
|
+
*/
|
|
793
|
+
export function AuditDataChange(resourceType: string, action: AuditAction) {
|
|
794
|
+
return Audit({
|
|
795
|
+
action,
|
|
796
|
+
category: AuditCategory.DATA_CHANGE,
|
|
797
|
+
resourceType,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Audit as security event
|
|
803
|
+
*/
|
|
804
|
+
export function AuditSecurity(description: string) {
|
|
805
|
+
return Audit({
|
|
806
|
+
category: AuditCategory.SECURITY,
|
|
807
|
+
description,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Audit as compliance event
|
|
813
|
+
*/
|
|
814
|
+
export function AuditCompliance(description: string) {
|
|
815
|
+
return Audit({
|
|
816
|
+
category: AuditCategory.COMPLIANCE,
|
|
817
|
+
description,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Sensitive operation (logs with extra detail)
|
|
823
|
+
*/
|
|
824
|
+
export function SensitiveOperation(description: string) {
|
|
825
|
+
return applyDecorators(
|
|
826
|
+
Audit({
|
|
827
|
+
category: AuditCategory.SECURITY,
|
|
828
|
+
description,
|
|
829
|
+
}),
|
|
830
|
+
SetMetadata('sensitiveOperation', true),
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
`;
|
|
834
|
+
}
|
|
835
|
+
function generateComplianceReporter() {
|
|
836
|
+
return `import { Injectable, Logger } from '@nestjs/common';
|
|
837
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
838
|
+
import { Repository, Between } from 'typeorm';
|
|
839
|
+
import { AuditLog, AuditAction, AuditCategory } from './audit-log.entity';
|
|
840
|
+
|
|
841
|
+
export interface ComplianceReport {
|
|
842
|
+
reportId: string;
|
|
843
|
+
generatedAt: Date;
|
|
844
|
+
period: { start: Date; end: Date };
|
|
845
|
+
summary: ComplianceSummary;
|
|
846
|
+
userActivity: UserActivityReport[];
|
|
847
|
+
dataAccess: DataAccessReport[];
|
|
848
|
+
securityEvents: SecurityEventReport[];
|
|
849
|
+
anomalies: AnomalyReport[];
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export interface ComplianceSummary {
|
|
853
|
+
totalEvents: number;
|
|
854
|
+
byCategory: Record<AuditCategory, number>;
|
|
855
|
+
byAction: Record<AuditAction, number>;
|
|
856
|
+
uniqueUsers: number;
|
|
857
|
+
uniqueResources: number;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export interface UserActivityReport {
|
|
861
|
+
userId: string;
|
|
862
|
+
totalActions: number;
|
|
863
|
+
lastActivity: Date;
|
|
864
|
+
actionBreakdown: Record<AuditAction, number>;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
export interface DataAccessReport {
|
|
868
|
+
resourceType: string;
|
|
869
|
+
accessCount: number;
|
|
870
|
+
uniqueUsers: number;
|
|
871
|
+
lastAccess: Date;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export interface SecurityEventReport {
|
|
875
|
+
type: string;
|
|
876
|
+
count: number;
|
|
877
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
878
|
+
details: string[];
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export interface AnomalyReport {
|
|
882
|
+
type: string;
|
|
883
|
+
description: string;
|
|
884
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
885
|
+
timestamp: Date;
|
|
886
|
+
context: any;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
@Injectable()
|
|
890
|
+
export class ComplianceReporter {
|
|
891
|
+
private readonly logger = new Logger(ComplianceReporter.name);
|
|
892
|
+
|
|
893
|
+
constructor(
|
|
894
|
+
@InjectRepository(AuditLog)
|
|
895
|
+
private readonly auditRepository: Repository<AuditLog>,
|
|
896
|
+
) {}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Generate compliance report for a period
|
|
900
|
+
*/
|
|
901
|
+
async generateReport(startDate: Date, endDate: Date): Promise<ComplianceReport> {
|
|
902
|
+
const logs = await this.auditRepository.find({
|
|
903
|
+
where: {
|
|
904
|
+
timestamp: Between(startDate, endDate),
|
|
905
|
+
},
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
const reportId = \`compliance_\${Date.now()}\`;
|
|
909
|
+
|
|
910
|
+
return {
|
|
911
|
+
reportId,
|
|
912
|
+
generatedAt: new Date(),
|
|
913
|
+
period: { start: startDate, end: endDate },
|
|
914
|
+
summary: this.generateSummary(logs),
|
|
915
|
+
userActivity: this.analyzeUserActivity(logs),
|
|
916
|
+
dataAccess: this.analyzeDataAccess(logs),
|
|
917
|
+
securityEvents: this.analyzeSecurityEvents(logs),
|
|
918
|
+
anomalies: this.detectAnomalies(logs),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Generate GDPR data subject report
|
|
924
|
+
*/
|
|
925
|
+
async generateGDPRReport(userId: string): Promise<any> {
|
|
926
|
+
const logs = await this.auditRepository.find({
|
|
927
|
+
where: { userId },
|
|
928
|
+
order: { timestamp: 'DESC' },
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
const dataAccessed = new Set<string>();
|
|
932
|
+
const dataModified = new Set<string>();
|
|
933
|
+
|
|
934
|
+
for (const log of logs) {
|
|
935
|
+
if (log.action === AuditAction.READ) {
|
|
936
|
+
dataAccessed.add(\`\${log.resourceType}:\${log.resourceId}\`);
|
|
937
|
+
}
|
|
938
|
+
if ([AuditAction.CREATE, AuditAction.UPDATE, AuditAction.DELETE].includes(log.action)) {
|
|
939
|
+
dataModified.add(\`\${log.resourceType}:\${log.resourceId}\`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
userId,
|
|
945
|
+
generatedAt: new Date(),
|
|
946
|
+
totalActivities: logs.length,
|
|
947
|
+
firstActivity: logs[logs.length - 1]?.timestamp,
|
|
948
|
+
lastActivity: logs[0]?.timestamp,
|
|
949
|
+
dataAccessed: Array.from(dataAccessed),
|
|
950
|
+
dataModified: Array.from(dataModified),
|
|
951
|
+
activities: logs.map(log => ({
|
|
952
|
+
timestamp: log.timestamp,
|
|
953
|
+
action: log.action,
|
|
954
|
+
resourceType: log.resourceType,
|
|
955
|
+
resourceId: log.resourceId,
|
|
956
|
+
description: log.description,
|
|
957
|
+
})),
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Generate SOC2 audit report
|
|
963
|
+
*/
|
|
964
|
+
async generateSOC2Report(startDate: Date, endDate: Date): Promise<any> {
|
|
965
|
+
const report = await this.generateReport(startDate, endDate);
|
|
966
|
+
|
|
967
|
+
return {
|
|
968
|
+
...report,
|
|
969
|
+
soc2Controls: {
|
|
970
|
+
accessControl: this.analyzeAccessControl(report),
|
|
971
|
+
changeManagement: this.analyzeChangeManagement(report),
|
|
972
|
+
incidentResponse: this.analyzeIncidentResponse(report),
|
|
973
|
+
dataProtection: this.analyzeDataProtection(report),
|
|
974
|
+
},
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
private generateSummary(logs: AuditLog[]): ComplianceSummary {
|
|
979
|
+
const byCategory: Record<AuditCategory, number> = {} as any;
|
|
980
|
+
const byAction: Record<AuditAction, number> = {} as any;
|
|
981
|
+
const users = new Set<string>();
|
|
982
|
+
const resources = new Set<string>();
|
|
983
|
+
|
|
984
|
+
for (const log of logs) {
|
|
985
|
+
byCategory[log.category] = (byCategory[log.category] || 0) + 1;
|
|
986
|
+
byAction[log.action] = (byAction[log.action] || 0) + 1;
|
|
987
|
+
if (log.userId) users.add(log.userId);
|
|
988
|
+
if (log.resourceId) resources.add(\`\${log.resourceType}:\${log.resourceId}\`);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return {
|
|
992
|
+
totalEvents: logs.length,
|
|
993
|
+
byCategory,
|
|
994
|
+
byAction,
|
|
995
|
+
uniqueUsers: users.size,
|
|
996
|
+
uniqueResources: resources.size,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private analyzeUserActivity(logs: AuditLog[]): UserActivityReport[] {
|
|
1001
|
+
const userMap = new Map<string, { actions: AuditLog[]; breakdown: Record<AuditAction, number> }>();
|
|
1002
|
+
|
|
1003
|
+
for (const log of logs) {
|
|
1004
|
+
if (!log.userId) continue;
|
|
1005
|
+
|
|
1006
|
+
const existing = userMap.get(log.userId) || { actions: [], breakdown: {} as any };
|
|
1007
|
+
existing.actions.push(log);
|
|
1008
|
+
existing.breakdown[log.action] = (existing.breakdown[log.action] || 0) + 1;
|
|
1009
|
+
userMap.set(log.userId, existing);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return Array.from(userMap.entries()).map(([userId, data]) => ({
|
|
1013
|
+
userId,
|
|
1014
|
+
totalActions: data.actions.length,
|
|
1015
|
+
lastActivity: data.actions[0]?.timestamp || new Date(),
|
|
1016
|
+
actionBreakdown: data.breakdown,
|
|
1017
|
+
}));
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
private analyzeDataAccess(logs: AuditLog[]): DataAccessReport[] {
|
|
1021
|
+
const resourceMap = new Map<string, { count: number; users: Set<string>; lastAccess: Date }>();
|
|
1022
|
+
|
|
1023
|
+
for (const log of logs) {
|
|
1024
|
+
if (log.action !== AuditAction.READ) continue;
|
|
1025
|
+
|
|
1026
|
+
const existing = resourceMap.get(log.resourceType) || {
|
|
1027
|
+
count: 0,
|
|
1028
|
+
users: new Set(),
|
|
1029
|
+
lastAccess: new Date(0),
|
|
1030
|
+
};
|
|
1031
|
+
existing.count++;
|
|
1032
|
+
if (log.userId) existing.users.add(log.userId);
|
|
1033
|
+
if (log.timestamp > existing.lastAccess) existing.lastAccess = log.timestamp;
|
|
1034
|
+
resourceMap.set(log.resourceType, existing);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return Array.from(resourceMap.entries()).map(([resourceType, data]) => ({
|
|
1038
|
+
resourceType,
|
|
1039
|
+
accessCount: data.count,
|
|
1040
|
+
uniqueUsers: data.users.size,
|
|
1041
|
+
lastAccess: data.lastAccess,
|
|
1042
|
+
}));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private analyzeSecurityEvents(logs: AuditLog[]): SecurityEventReport[] {
|
|
1046
|
+
const securityLogs = logs.filter(l => l.category === AuditCategory.SECURITY);
|
|
1047
|
+
const events: SecurityEventReport[] = [];
|
|
1048
|
+
|
|
1049
|
+
// Failed logins
|
|
1050
|
+
const failedLogins = securityLogs.filter(
|
|
1051
|
+
l => l.action === AuditAction.LOGIN && l.metadata?.success === false
|
|
1052
|
+
);
|
|
1053
|
+
if (failedLogins.length > 0) {
|
|
1054
|
+
events.push({
|
|
1055
|
+
type: 'Failed Login Attempts',
|
|
1056
|
+
count: failedLogins.length,
|
|
1057
|
+
severity: failedLogins.length > 10 ? 'high' : 'medium',
|
|
1058
|
+
details: failedLogins.slice(0, 5).map(l => l.description),
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
return events;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
private detectAnomalies(logs: AuditLog[]): AnomalyReport[] {
|
|
1066
|
+
const anomalies: AnomalyReport[] = [];
|
|
1067
|
+
|
|
1068
|
+
// Detect unusual access patterns
|
|
1069
|
+
const userActivityByHour = new Map<string, Map<number, number>>();
|
|
1070
|
+
|
|
1071
|
+
for (const log of logs) {
|
|
1072
|
+
if (!log.userId) continue;
|
|
1073
|
+
const hour = new Date(log.timestamp).getHours();
|
|
1074
|
+
const userHours = userActivityByHour.get(log.userId) || new Map();
|
|
1075
|
+
userHours.set(hour, (userHours.get(hour) || 0) + 1);
|
|
1076
|
+
userActivityByHour.set(log.userId, userHours);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Check for unusual activity hours (outside 6am-11pm)
|
|
1080
|
+
for (const [userId, hourMap] of userActivityByHour.entries()) {
|
|
1081
|
+
const unusualHours = Array.from(hourMap.entries())
|
|
1082
|
+
.filter(([hour]) => hour < 6 || hour > 23)
|
|
1083
|
+
.reduce((sum, [, count]) => sum + count, 0);
|
|
1084
|
+
|
|
1085
|
+
if (unusualHours > 10) {
|
|
1086
|
+
anomalies.push({
|
|
1087
|
+
type: 'Unusual Access Hours',
|
|
1088
|
+
description: \`User \${userId} had \${unusualHours} activities outside normal hours\`,
|
|
1089
|
+
severity: 'medium',
|
|
1090
|
+
timestamp: new Date(),
|
|
1091
|
+
context: { userId, unusualHours },
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return anomalies;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private analyzeAccessControl(report: ComplianceReport): any {
|
|
1100
|
+
return {
|
|
1101
|
+
status: 'compliant',
|
|
1102
|
+
findings: [],
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
private analyzeChangeManagement(report: ComplianceReport): any {
|
|
1107
|
+
return {
|
|
1108
|
+
status: 'compliant',
|
|
1109
|
+
findings: [],
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
private analyzeIncidentResponse(report: ComplianceReport): any {
|
|
1114
|
+
return {
|
|
1115
|
+
status: 'compliant',
|
|
1116
|
+
findings: [],
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private analyzeDataProtection(report: ComplianceReport): any {
|
|
1121
|
+
return {
|
|
1122
|
+
status: 'compliant',
|
|
1123
|
+
findings: [],
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
`;
|
|
1128
|
+
}
|
|
1129
|
+
//# sourceMappingURL=audit-logging.js.map
|