nestjs-ddd-cli 2.2.1 → 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 +24 -5
- 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,2233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Comprehensive Security Patterns Generator
|
|
4
|
+
* RBAC, encryption, OWASP protections, secret management
|
|
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.setupSecurityPatterns = setupSecurityPatterns;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
async function setupSecurityPatterns(basePath, options = {}) {
|
|
48
|
+
console.log(chalk_1.default.bold.blue('\n🔐 Setting up Security Patterns\n'));
|
|
49
|
+
const sharedPath = path.join(basePath, 'src/shared/security');
|
|
50
|
+
if (!fs.existsSync(sharedPath)) {
|
|
51
|
+
fs.mkdirSync(sharedPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
fs.writeFileSync(path.join(sharedPath, 'encryption.service.ts'), generateEncryptionService());
|
|
54
|
+
console.log(chalk_1.default.green(` ✓ Created encryption service`));
|
|
55
|
+
fs.writeFileSync(path.join(sharedPath, 'rbac.service.ts'), generateRbacService());
|
|
56
|
+
console.log(chalk_1.default.green(` ✓ Created RBAC service`));
|
|
57
|
+
fs.writeFileSync(path.join(sharedPath, 'rbac.decorator.ts'), generateRbacDecorator());
|
|
58
|
+
console.log(chalk_1.default.green(` ✓ Created RBAC decorator`));
|
|
59
|
+
fs.writeFileSync(path.join(sharedPath, 'owasp.middleware.ts'), generateOwaspMiddleware());
|
|
60
|
+
console.log(chalk_1.default.green(` ✓ Created OWASP middleware`));
|
|
61
|
+
fs.writeFileSync(path.join(sharedPath, 'secret-vault.service.ts'), generateSecretVault());
|
|
62
|
+
console.log(chalk_1.default.green(` ✓ Created secret vault`));
|
|
63
|
+
fs.writeFileSync(path.join(sharedPath, 'input-sanitizer.ts'), generateInputSanitizer());
|
|
64
|
+
console.log(chalk_1.default.green(` ✓ Created input sanitizer`));
|
|
65
|
+
fs.writeFileSync(path.join(sharedPath, 'security.module.ts'), generateSecurityModule());
|
|
66
|
+
console.log(chalk_1.default.green(` ✓ Created security module`));
|
|
67
|
+
fs.writeFileSync(path.join(sharedPath, 'cors.config.ts'), generateCorsConfig());
|
|
68
|
+
console.log(chalk_1.default.green(` ✓ Created CORS configuration`));
|
|
69
|
+
fs.writeFileSync(path.join(sharedPath, 'cookie.config.ts'), generateCookieConfig());
|
|
70
|
+
console.log(chalk_1.default.green(` ✓ Created cookie security configuration`));
|
|
71
|
+
fs.writeFileSync(path.join(sharedPath, 'jwt.security.ts'), generateJwtSecurity());
|
|
72
|
+
console.log(chalk_1.default.green(` ✓ Created JWT security service`));
|
|
73
|
+
fs.writeFileSync(path.join(sharedPath, 'security-headers.config.ts'), generateSecurityHeadersConfig());
|
|
74
|
+
console.log(chalk_1.default.green(` ✓ Created security headers configuration`));
|
|
75
|
+
console.log(chalk_1.default.bold.green('\n✅ Security patterns ready!\n'));
|
|
76
|
+
}
|
|
77
|
+
function generateEncryptionService() {
|
|
78
|
+
return `/**
|
|
79
|
+
* Encryption Service
|
|
80
|
+
* AES-256-GCM authenticated encryption for data at rest
|
|
81
|
+
* OWASP A02:2021 compliant
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
85
|
+
import * as crypto from 'crypto';
|
|
86
|
+
|
|
87
|
+
export interface EncryptionOptions {
|
|
88
|
+
algorithm?: string;
|
|
89
|
+
keyLength?: number;
|
|
90
|
+
ivLength?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Minimum password/key requirements
|
|
94
|
+
const MIN_KEY_LENGTH = 16;
|
|
95
|
+
const MIN_PASSWORD_LENGTH = 8;
|
|
96
|
+
const MAX_PLAINTEXT_LENGTH = 10 * 1024 * 1024; // 10MB max
|
|
97
|
+
|
|
98
|
+
@Injectable()
|
|
99
|
+
export class EncryptionService {
|
|
100
|
+
private readonly logger = new Logger(EncryptionService.name);
|
|
101
|
+
private readonly algorithm = 'aes-256-gcm';
|
|
102
|
+
private readonly keyLength = 32;
|
|
103
|
+
private readonly ivLength = 16;
|
|
104
|
+
private readonly tagLength = 16;
|
|
105
|
+
private readonly saltLength = 32;
|
|
106
|
+
|
|
107
|
+
// PBKDF2 iterations - OWASP recommends at least 600,000 for SHA-256
|
|
108
|
+
private readonly pbkdf2Iterations = 600000;
|
|
109
|
+
private readonly passwordHashIterations = 310000;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Encrypt data with AES-256-GCM
|
|
113
|
+
* @throws Error if key is too short or plaintext too long
|
|
114
|
+
*/
|
|
115
|
+
encrypt(plaintext: string, key: string): string {
|
|
116
|
+
// Input validation
|
|
117
|
+
if (!plaintext || typeof plaintext !== 'string') {
|
|
118
|
+
throw new Error('Plaintext must be a non-empty string');
|
|
119
|
+
}
|
|
120
|
+
if (!key || typeof key !== 'string' || key.length < MIN_KEY_LENGTH) {
|
|
121
|
+
throw new Error(\`Encryption key must be at least \${MIN_KEY_LENGTH} characters\`);
|
|
122
|
+
}
|
|
123
|
+
if (plaintext.length > MAX_PLAINTEXT_LENGTH) {
|
|
124
|
+
throw new Error(\`Plaintext exceeds maximum length of \${MAX_PLAINTEXT_LENGTH} bytes\`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const iv = crypto.randomBytes(this.ivLength);
|
|
128
|
+
const salt = crypto.randomBytes(this.saltLength);
|
|
129
|
+
const derivedKey = this.deriveKey(key, salt);
|
|
130
|
+
|
|
131
|
+
const cipher = crypto.createCipheriv(this.algorithm, derivedKey, iv);
|
|
132
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|
133
|
+
encrypted += cipher.final('hex');
|
|
134
|
+
|
|
135
|
+
const tag = cipher.getAuthTag();
|
|
136
|
+
|
|
137
|
+
// Format: version:salt:iv:tag:encrypted (version for future algorithm changes)
|
|
138
|
+
return [
|
|
139
|
+
'v1',
|
|
140
|
+
salt.toString('hex'),
|
|
141
|
+
iv.toString('hex'),
|
|
142
|
+
tag.toString('hex'),
|
|
143
|
+
encrypted,
|
|
144
|
+
].join(':');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Decrypt data
|
|
149
|
+
* @throws Error if decryption fails (tampered or wrong key)
|
|
150
|
+
*/
|
|
151
|
+
decrypt(ciphertext: string, key: string): string {
|
|
152
|
+
if (!ciphertext || typeof ciphertext !== 'string') {
|
|
153
|
+
throw new Error('Ciphertext must be a non-empty string');
|
|
154
|
+
}
|
|
155
|
+
if (!key || typeof key !== 'string' || key.length < MIN_KEY_LENGTH) {
|
|
156
|
+
throw new Error(\`Decryption key must be at least \${MIN_KEY_LENGTH} characters\`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const parts = ciphertext.split(':');
|
|
160
|
+
|
|
161
|
+
// Support versioned format
|
|
162
|
+
let saltHex: string, ivHex: string, tagHex: string, encrypted: string;
|
|
163
|
+
|
|
164
|
+
if (parts[0] === 'v1' && parts.length === 5) {
|
|
165
|
+
[, saltHex, ivHex, tagHex, encrypted] = parts;
|
|
166
|
+
} else if (parts.length === 4) {
|
|
167
|
+
// Legacy format without version
|
|
168
|
+
[saltHex, ivHex, tagHex, encrypted] = parts;
|
|
169
|
+
} else {
|
|
170
|
+
throw new Error('Invalid ciphertext format');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const salt = Buffer.from(saltHex, 'hex');
|
|
175
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
176
|
+
const tag = Buffer.from(tagHex, 'hex');
|
|
177
|
+
const derivedKey = this.deriveKey(key, salt);
|
|
178
|
+
|
|
179
|
+
const decipher = crypto.createDecipheriv(this.algorithm, derivedKey, iv);
|
|
180
|
+
decipher.setAuthTag(tag);
|
|
181
|
+
|
|
182
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
183
|
+
decrypted += decipher.final('utf8');
|
|
184
|
+
|
|
185
|
+
return decrypted;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// Don't reveal specific error details
|
|
188
|
+
this.logger.warn('Decryption failed - possibly tampered data or wrong key');
|
|
189
|
+
throw new Error('Decryption failed');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Hash password using PBKDF2 with high iteration count
|
|
195
|
+
* OWASP recommendation: 310,000 iterations for PBKDF2-HMAC-SHA256
|
|
196
|
+
*/
|
|
197
|
+
async hashPassword(password: string): Promise<string> {
|
|
198
|
+
if (!password || typeof password !== 'string') {
|
|
199
|
+
throw new Error('Password must be a non-empty string');
|
|
200
|
+
}
|
|
201
|
+
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
202
|
+
throw new Error(\`Password must be at least \${MIN_PASSWORD_LENGTH} characters\`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const salt = crypto.randomBytes(this.saltLength);
|
|
206
|
+
const iterations = this.passwordHashIterations;
|
|
207
|
+
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
crypto.pbkdf2(password, salt, iterations, 64, 'sha512', (err, key) => {
|
|
210
|
+
if (err) reject(err);
|
|
211
|
+
// Include algorithm info for future migration
|
|
212
|
+
resolve(\`pbkdf2:sha512:\${iterations}:\${salt.toString('hex')}:\${key.toString('hex')}\`);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Verify password with timing-safe comparison
|
|
219
|
+
*/
|
|
220
|
+
async verifyPassword(password: string, hash: string): Promise<boolean> {
|
|
221
|
+
if (!password || !hash) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const parts = hash.split(':');
|
|
227
|
+
let saltHex: string, keyHex: string, iterations: number;
|
|
228
|
+
|
|
229
|
+
// Support new format with algorithm prefix
|
|
230
|
+
if (parts[0] === 'pbkdf2' && parts.length === 5) {
|
|
231
|
+
[, , iterations, saltHex, keyHex] = [parts[0], parts[1], parseInt(parts[2], 10), parts[3], parts[4]] as [string, string, number, string, string];
|
|
232
|
+
} else if (parts.length === 3) {
|
|
233
|
+
// Legacy format
|
|
234
|
+
[saltHex, iterations, keyHex] = [parts[0], parseInt(parts[1], 10), parts[2]] as [string, number, string];
|
|
235
|
+
} else {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const salt = Buffer.from(saltHex, 'hex');
|
|
240
|
+
const storedKey = Buffer.from(keyHex, 'hex');
|
|
241
|
+
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
crypto.pbkdf2(password, salt, iterations, 64, 'sha512', (err, derivedKey) => {
|
|
244
|
+
if (err) {
|
|
245
|
+
reject(err);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
// Timing-safe comparison prevents timing attacks
|
|
249
|
+
try {
|
|
250
|
+
resolve(crypto.timingSafeEqual(storedKey, derivedKey));
|
|
251
|
+
} catch {
|
|
252
|
+
resolve(false);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Generate cryptographically secure random token
|
|
263
|
+
*/
|
|
264
|
+
generateToken(length: number = 32): string {
|
|
265
|
+
if (length < 16) {
|
|
266
|
+
throw new Error('Token length must be at least 16 bytes');
|
|
267
|
+
}
|
|
268
|
+
return crypto.randomBytes(length).toString('hex');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Generate API key with checksum
|
|
273
|
+
*/
|
|
274
|
+
generateApiKey(prefix: string = 'sk'): string {
|
|
275
|
+
const sanitizedPrefix = prefix.replace(/[^a-zA-Z0-9]/g, '').substring(0, 10);
|
|
276
|
+
const key = crypto.randomBytes(24).toString('base64url');
|
|
277
|
+
const checksum = this.hash(key).substring(0, 4);
|
|
278
|
+
return \`\${sanitizedPrefix}_\${key}_\${checksum}\`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Verify API key checksum
|
|
283
|
+
*/
|
|
284
|
+
verifyApiKeyChecksum(apiKey: string): boolean {
|
|
285
|
+
const parts = apiKey.split('_');
|
|
286
|
+
if (parts.length !== 3) return false;
|
|
287
|
+
|
|
288
|
+
const [, key, checksum] = parts;
|
|
289
|
+
const expectedChecksum = this.hash(key).substring(0, 4);
|
|
290
|
+
|
|
291
|
+
return crypto.timingSafeEqual(
|
|
292
|
+
Buffer.from(checksum),
|
|
293
|
+
Buffer.from(expectedChecksum)
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Hash data (one-way) - defaults to SHA-256
|
|
299
|
+
*/
|
|
300
|
+
hash(data: string, algorithm: string = 'sha256'): string {
|
|
301
|
+
const allowedAlgorithms = ['sha256', 'sha384', 'sha512', 'sha3-256', 'sha3-512'];
|
|
302
|
+
if (!allowedAlgorithms.includes(algorithm)) {
|
|
303
|
+
throw new Error(\`Algorithm must be one of: \${allowedAlgorithms.join(', ')}\`);
|
|
304
|
+
}
|
|
305
|
+
return crypto.createHash(algorithm).update(data).digest('hex');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* HMAC signature using SHA-256
|
|
310
|
+
*/
|
|
311
|
+
hmac(data: string, secret: string): string {
|
|
312
|
+
if (!secret || secret.length < MIN_KEY_LENGTH) {
|
|
313
|
+
throw new Error(\`HMAC secret must be at least \${MIN_KEY_LENGTH} characters\`);
|
|
314
|
+
}
|
|
315
|
+
return crypto.createHmac('sha256', secret).update(data).digest('hex');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Verify HMAC with timing-safe comparison
|
|
320
|
+
*/
|
|
321
|
+
verifyHmac(data: string, signature: string, secret: string): boolean {
|
|
322
|
+
try {
|
|
323
|
+
const expected = this.hmac(data, secret);
|
|
324
|
+
// Ensure both are same length for timing-safe comparison
|
|
325
|
+
if (signature.length !== expected.length) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
329
|
+
} catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Securely compare two strings (timing-safe)
|
|
336
|
+
*/
|
|
337
|
+
secureCompare(a: string, b: string): boolean {
|
|
338
|
+
if (typeof a !== 'string' || typeof b !== 'string') {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
if (a.length !== b.length) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private deriveKey(password: string, salt: Buffer): Buffer {
|
|
348
|
+
return crypto.pbkdf2Sync(password, salt, this.pbkdf2Iterations, this.keyLength, 'sha256');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Field-level encryption decorator
|
|
354
|
+
*/
|
|
355
|
+
export function Encrypted(): PropertyDecorator {
|
|
356
|
+
return function (target: Object, propertyKey: string | symbol) {
|
|
357
|
+
Reflect.defineMetadata('encrypted', true, target, propertyKey);
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
function generateRbacService() {
|
|
363
|
+
return `/**
|
|
364
|
+
* RBAC (Role-Based Access Control) Service
|
|
365
|
+
* Manages roles, permissions, and access policies
|
|
366
|
+
*/
|
|
367
|
+
|
|
368
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
369
|
+
|
|
370
|
+
export interface Role {
|
|
371
|
+
name: string;
|
|
372
|
+
permissions: string[];
|
|
373
|
+
inherits?: string[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export interface Permission {
|
|
377
|
+
resource: string;
|
|
378
|
+
action: string;
|
|
379
|
+
conditions?: PermissionCondition[];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export interface PermissionCondition {
|
|
383
|
+
field: string;
|
|
384
|
+
operator: 'equals' | 'in' | 'owns' | 'custom';
|
|
385
|
+
value?: any;
|
|
386
|
+
handler?: (user: any, resource: any) => boolean;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export interface AccessContext {
|
|
390
|
+
user: {
|
|
391
|
+
id: string;
|
|
392
|
+
roles: string[];
|
|
393
|
+
attributes?: Record<string, any>;
|
|
394
|
+
};
|
|
395
|
+
resource?: any;
|
|
396
|
+
action: string;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
@Injectable()
|
|
400
|
+
export class RbacService {
|
|
401
|
+
private readonly logger = new Logger(RbacService.name);
|
|
402
|
+
private readonly roles = new Map<string, Role>();
|
|
403
|
+
private readonly permissions = new Map<string, Permission>();
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Define a role
|
|
407
|
+
*/
|
|
408
|
+
defineRole(role: Role): void {
|
|
409
|
+
this.roles.set(role.name, role);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Define a permission
|
|
414
|
+
*/
|
|
415
|
+
definePermission(name: string, permission: Permission): void {
|
|
416
|
+
this.permissions.set(name, permission);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Check if user has permission
|
|
421
|
+
*/
|
|
422
|
+
hasPermission(context: AccessContext): boolean {
|
|
423
|
+
const { user, action, resource } = context;
|
|
424
|
+
|
|
425
|
+
// Get all permissions for user's roles
|
|
426
|
+
const userPermissions = this.getUserPermissions(user.roles);
|
|
427
|
+
|
|
428
|
+
// Check each permission
|
|
429
|
+
for (const permName of userPermissions) {
|
|
430
|
+
const permission = this.permissions.get(permName);
|
|
431
|
+
if (!permission) continue;
|
|
432
|
+
|
|
433
|
+
if (permission.action !== action && permission.action !== '*') continue;
|
|
434
|
+
|
|
435
|
+
// Check conditions
|
|
436
|
+
if (permission.conditions && permission.conditions.length > 0) {
|
|
437
|
+
if (this.evaluateConditions(permission.conditions, user, resource)) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Get all permissions for roles (including inherited)
|
|
450
|
+
*/
|
|
451
|
+
getUserPermissions(roleNames: string[]): string[] {
|
|
452
|
+
const permissions = new Set<string>();
|
|
453
|
+
const processedRoles = new Set<string>();
|
|
454
|
+
|
|
455
|
+
const processRole = (roleName: string) => {
|
|
456
|
+
if (processedRoles.has(roleName)) return;
|
|
457
|
+
processedRoles.add(roleName);
|
|
458
|
+
|
|
459
|
+
const role = this.roles.get(roleName);
|
|
460
|
+
if (!role) return;
|
|
461
|
+
|
|
462
|
+
role.permissions.forEach(p => permissions.add(p));
|
|
463
|
+
|
|
464
|
+
if (role.inherits) {
|
|
465
|
+
role.inherits.forEach(processRole);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
roleNames.forEach(processRole);
|
|
470
|
+
return Array.from(permissions);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Check if user can perform action on resource
|
|
475
|
+
*/
|
|
476
|
+
can(user: { id: string; roles: string[] }, action: string, resource?: any): boolean {
|
|
477
|
+
return this.hasPermission({ user, action, resource });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Check if user owns resource
|
|
482
|
+
*/
|
|
483
|
+
owns(user: { id: string }, resource: { userId?: string; ownerId?: string }): boolean {
|
|
484
|
+
return resource.userId === user.id || resource.ownerId === user.id;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private evaluateConditions(
|
|
488
|
+
conditions: PermissionCondition[],
|
|
489
|
+
user: any,
|
|
490
|
+
resource: any,
|
|
491
|
+
): boolean {
|
|
492
|
+
return conditions.every(condition => {
|
|
493
|
+
switch (condition.operator) {
|
|
494
|
+
case 'equals':
|
|
495
|
+
return resource?.[condition.field] === condition.value;
|
|
496
|
+
case 'in':
|
|
497
|
+
return Array.isArray(condition.value) &&
|
|
498
|
+
condition.value.includes(resource?.[condition.field]);
|
|
499
|
+
case 'owns':
|
|
500
|
+
return this.owns(user, resource);
|
|
501
|
+
case 'custom':
|
|
502
|
+
return condition.handler?.(user, resource) ?? false;
|
|
503
|
+
default:
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Get role by name
|
|
511
|
+
*/
|
|
512
|
+
getRole(name: string): Role | undefined {
|
|
513
|
+
return this.roles.get(name);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* List all roles
|
|
518
|
+
*/
|
|
519
|
+
listRoles(): Role[] {
|
|
520
|
+
return Array.from(this.roles.values());
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* ABAC (Attribute-Based Access Control) Service
|
|
526
|
+
*/
|
|
527
|
+
@Injectable()
|
|
528
|
+
export class AbacService {
|
|
529
|
+
private readonly policies: AbacPolicy[] = [];
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Add policy
|
|
533
|
+
*/
|
|
534
|
+
addPolicy(policy: AbacPolicy): void {
|
|
535
|
+
this.policies.push(policy);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Evaluate access
|
|
540
|
+
*/
|
|
541
|
+
evaluate(context: AbacContext): boolean {
|
|
542
|
+
for (const policy of this.policies) {
|
|
543
|
+
if (policy.effect === 'deny' && this.matchesPolicy(policy, context)) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
for (const policy of this.policies) {
|
|
549
|
+
if (policy.effect === 'allow' && this.matchesPolicy(policy, context)) {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return false; // Default deny
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private matchesPolicy(policy: AbacPolicy, context: AbacContext): boolean {
|
|
558
|
+
// Check subject conditions
|
|
559
|
+
if (policy.subject && !this.matchConditions(policy.subject, context.subject)) {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Check resource conditions
|
|
564
|
+
if (policy.resource && !this.matchConditions(policy.resource, context.resource)) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Check action
|
|
569
|
+
if (policy.action && policy.action !== context.action && policy.action !== '*') {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Check environment
|
|
574
|
+
if (policy.environment && !this.matchConditions(policy.environment, context.environment)) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private matchConditions(
|
|
582
|
+
conditions: Record<string, any>,
|
|
583
|
+
values: Record<string, any>,
|
|
584
|
+
): boolean {
|
|
585
|
+
for (const [key, expected] of Object.entries(conditions)) {
|
|
586
|
+
const actual = values[key];
|
|
587
|
+
if (Array.isArray(expected)) {
|
|
588
|
+
if (!expected.includes(actual)) return false;
|
|
589
|
+
} else if (expected !== actual) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
interface AbacPolicy {
|
|
598
|
+
name: string;
|
|
599
|
+
effect: 'allow' | 'deny';
|
|
600
|
+
subject?: Record<string, any>;
|
|
601
|
+
resource?: Record<string, any>;
|
|
602
|
+
action?: string;
|
|
603
|
+
environment?: Record<string, any>;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
interface AbacContext {
|
|
607
|
+
subject: Record<string, any>;
|
|
608
|
+
resource: Record<string, any>;
|
|
609
|
+
action: string;
|
|
610
|
+
environment?: Record<string, any>;
|
|
611
|
+
}
|
|
612
|
+
`;
|
|
613
|
+
}
|
|
614
|
+
function generateRbacDecorator() {
|
|
615
|
+
return `/**
|
|
616
|
+
* RBAC Decorators
|
|
617
|
+
* Declarative access control on routes
|
|
618
|
+
*/
|
|
619
|
+
|
|
620
|
+
import { SetMetadata, applyDecorators, UseGuards } from '@nestjs/common';
|
|
621
|
+
import {
|
|
622
|
+
Injectable,
|
|
623
|
+
CanActivate,
|
|
624
|
+
ExecutionContext,
|
|
625
|
+
ForbiddenException,
|
|
626
|
+
} from '@nestjs/common';
|
|
627
|
+
import { Reflector } from '@nestjs/core';
|
|
628
|
+
import { RbacService } from './rbac.service';
|
|
629
|
+
|
|
630
|
+
export const ROLES_KEY = 'roles';
|
|
631
|
+
export const PERMISSIONS_KEY = 'permissions';
|
|
632
|
+
export const RESOURCE_KEY = 'resource';
|
|
633
|
+
export const OWNERSHIP_KEY = 'ownership';
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Require specific roles
|
|
637
|
+
*/
|
|
638
|
+
export function Roles(...roles: string[]) {
|
|
639
|
+
return SetMetadata(ROLES_KEY, roles);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Require specific permissions
|
|
644
|
+
*/
|
|
645
|
+
export function Permissions(...permissions: string[]) {
|
|
646
|
+
return SetMetadata(PERMISSIONS_KEY, permissions);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Require resource ownership
|
|
651
|
+
*/
|
|
652
|
+
export function RequireOwnership(resourceParam: string = 'id') {
|
|
653
|
+
return applyDecorators(
|
|
654
|
+
SetMetadata(OWNERSHIP_KEY, resourceParam),
|
|
655
|
+
UseGuards(OwnershipGuard),
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Public route (no auth required)
|
|
661
|
+
*/
|
|
662
|
+
export function Public() {
|
|
663
|
+
return SetMetadata('isPublic', true);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Roles Guard
|
|
668
|
+
*/
|
|
669
|
+
@Injectable()
|
|
670
|
+
export class RolesGuard implements CanActivate {
|
|
671
|
+
constructor(
|
|
672
|
+
private readonly reflector: Reflector,
|
|
673
|
+
private readonly rbacService: RbacService,
|
|
674
|
+
) {}
|
|
675
|
+
|
|
676
|
+
canActivate(context: ExecutionContext): boolean {
|
|
677
|
+
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
|
|
678
|
+
context.getHandler(),
|
|
679
|
+
context.getClass(),
|
|
680
|
+
]);
|
|
681
|
+
|
|
682
|
+
if (!requiredRoles || requiredRoles.length === 0) {
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const request = context.switchToHttp().getRequest();
|
|
687
|
+
const user = request.user;
|
|
688
|
+
|
|
689
|
+
if (!user || !user.roles) {
|
|
690
|
+
throw new ForbiddenException('No roles assigned');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const hasRole = requiredRoles.some(role => user.roles.includes(role));
|
|
694
|
+
|
|
695
|
+
if (!hasRole) {
|
|
696
|
+
throw new ForbiddenException('Insufficient role privileges');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Permissions Guard
|
|
705
|
+
*/
|
|
706
|
+
@Injectable()
|
|
707
|
+
export class PermissionsGuard implements CanActivate {
|
|
708
|
+
constructor(
|
|
709
|
+
private readonly reflector: Reflector,
|
|
710
|
+
private readonly rbacService: RbacService,
|
|
711
|
+
) {}
|
|
712
|
+
|
|
713
|
+
canActivate(context: ExecutionContext): boolean {
|
|
714
|
+
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [
|
|
715
|
+
context.getHandler(),
|
|
716
|
+
context.getClass(),
|
|
717
|
+
]);
|
|
718
|
+
|
|
719
|
+
if (!requiredPermissions || requiredPermissions.length === 0) {
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const request = context.switchToHttp().getRequest();
|
|
724
|
+
const user = request.user;
|
|
725
|
+
|
|
726
|
+
if (!user) {
|
|
727
|
+
throw new ForbiddenException('Not authenticated');
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const userPermissions = this.rbacService.getUserPermissions(user.roles || []);
|
|
731
|
+
|
|
732
|
+
const hasAllPermissions = requiredPermissions.every(
|
|
733
|
+
perm => userPermissions.includes(perm) || userPermissions.includes('*'),
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
if (!hasAllPermissions) {
|
|
737
|
+
throw new ForbiddenException('Insufficient permissions');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Ownership Guard
|
|
746
|
+
*/
|
|
747
|
+
@Injectable()
|
|
748
|
+
export class OwnershipGuard implements CanActivate {
|
|
749
|
+
constructor(private readonly reflector: Reflector) {}
|
|
750
|
+
|
|
751
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
752
|
+
const resourceParam = this.reflector.get<string>(OWNERSHIP_KEY, context.getHandler());
|
|
753
|
+
|
|
754
|
+
if (!resourceParam) {
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const request = context.switchToHttp().getRequest();
|
|
759
|
+
const user = request.user;
|
|
760
|
+
const resourceId = request.params[resourceParam];
|
|
761
|
+
|
|
762
|
+
if (!user) {
|
|
763
|
+
throw new ForbiddenException('Not authenticated');
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// This would need to fetch the resource and check ownership
|
|
767
|
+
// For now, we'll just check if the IDs match
|
|
768
|
+
if (resourceId !== user.id) {
|
|
769
|
+
throw new ForbiddenException('You do not own this resource');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
`;
|
|
776
|
+
}
|
|
777
|
+
function generateOwaspMiddleware() {
|
|
778
|
+
return `/**
|
|
779
|
+
* OWASP Security Middleware
|
|
780
|
+
* Protection against common web vulnerabilities
|
|
781
|
+
*/
|
|
782
|
+
|
|
783
|
+
import { Injectable, NestMiddleware, BadRequestException } from '@nestjs/common';
|
|
784
|
+
import { Request, Response, NextFunction } from 'express';
|
|
785
|
+
|
|
786
|
+
@Injectable()
|
|
787
|
+
export class OwaspMiddleware implements NestMiddleware {
|
|
788
|
+
use(req: Request, res: Response, next: NextFunction): void {
|
|
789
|
+
// Set security headers
|
|
790
|
+
this.setSecurityHeaders(res);
|
|
791
|
+
|
|
792
|
+
// Validate request
|
|
793
|
+
this.validateRequest(req);
|
|
794
|
+
|
|
795
|
+
next();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private setSecurityHeaders(res: Response): void {
|
|
799
|
+
// Prevent clickjacking
|
|
800
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
801
|
+
|
|
802
|
+
// XSS protection
|
|
803
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
804
|
+
|
|
805
|
+
// Prevent MIME sniffing
|
|
806
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
807
|
+
|
|
808
|
+
// Content Security Policy
|
|
809
|
+
res.setHeader(
|
|
810
|
+
'Content-Security-Policy',
|
|
811
|
+
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'",
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
// HSTS
|
|
815
|
+
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
816
|
+
|
|
817
|
+
// Referrer Policy
|
|
818
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
819
|
+
|
|
820
|
+
// Permissions Policy
|
|
821
|
+
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private validateRequest(req: Request): void {
|
|
825
|
+
// Check for common attack patterns
|
|
826
|
+
this.checkSqlInjection(req);
|
|
827
|
+
this.checkXss(req);
|
|
828
|
+
this.checkPathTraversal(req);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private checkSqlInjection(req: Request): void {
|
|
832
|
+
const sqlPatterns = [
|
|
833
|
+
/(\\'|\\")\\s*OR\\s+/i,
|
|
834
|
+
/UNION\\s+SELECT/i,
|
|
835
|
+
/DROP\\s+TABLE/i,
|
|
836
|
+
/INSERT\\s+INTO/i,
|
|
837
|
+
/DELETE\\s+FROM/i,
|
|
838
|
+
/UPDATE\\s+\\w+\\s+SET/i,
|
|
839
|
+
/--/,
|
|
840
|
+
/;\\s*$/,
|
|
841
|
+
];
|
|
842
|
+
|
|
843
|
+
const checkValue = (value: any): void => {
|
|
844
|
+
if (typeof value === 'string') {
|
|
845
|
+
for (const pattern of sqlPatterns) {
|
|
846
|
+
if (pattern.test(value)) {
|
|
847
|
+
throw new BadRequestException('Potential SQL injection detected');
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
851
|
+
Object.values(value).forEach(checkValue);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
checkValue(req.query);
|
|
856
|
+
checkValue(req.body);
|
|
857
|
+
checkValue(req.params);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
private checkXss(req: Request): void {
|
|
861
|
+
const xssPatterns = [
|
|
862
|
+
// Script tags and attributes
|
|
863
|
+
/<script\\b[^>]*>/i,
|
|
864
|
+
/<\\/script>/i,
|
|
865
|
+
// JavaScript protocol
|
|
866
|
+
/javascript:/i,
|
|
867
|
+
/vbscript:/i,
|
|
868
|
+
// Event handlers
|
|
869
|
+
/on\\w+\\s*=/i,
|
|
870
|
+
// Dangerous HTML elements
|
|
871
|
+
/<iframe/i,
|
|
872
|
+
/<object/i,
|
|
873
|
+
/<embed/i,
|
|
874
|
+
/<svg[^>]*onload/i,
|
|
875
|
+
/<img[^>]*onerror/i,
|
|
876
|
+
/<body[^>]*onload/i,
|
|
877
|
+
// Data URIs with HTML/Script
|
|
878
|
+
/data:\\s*text\\/html/i,
|
|
879
|
+
/data:\\s*application\\/javascript/i,
|
|
880
|
+
// CSS injection
|
|
881
|
+
/expression\\s*\\(/i,
|
|
882
|
+
/-moz-binding/i,
|
|
883
|
+
// HTML5 attack vectors
|
|
884
|
+
/<math/i,
|
|
885
|
+
/<video[^>]*on/i,
|
|
886
|
+
/<audio[^>]*on/i,
|
|
887
|
+
// Template injection
|
|
888
|
+
/\\{\\{.*\\}\\}/,
|
|
889
|
+
/\\$\\{.*\\}/,
|
|
890
|
+
];
|
|
891
|
+
|
|
892
|
+
const checkValue = (value: any): void => {
|
|
893
|
+
if (typeof value === 'string') {
|
|
894
|
+
for (const pattern of xssPatterns) {
|
|
895
|
+
if (pattern.test(value)) {
|
|
896
|
+
throw new BadRequestException('Potential XSS attack detected');
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
900
|
+
Object.values(value).forEach(checkValue);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
checkValue(req.query);
|
|
905
|
+
checkValue(req.body);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private checkPathTraversal(req: Request): void {
|
|
909
|
+
const pathTraversalPatterns = [
|
|
910
|
+
/\\.\\.\\//, // ../
|
|
911
|
+
/\\.\\.\\\\/, // ..\\
|
|
912
|
+
/%2e%2e%2f/i, // encoded ../
|
|
913
|
+
/%2e%2e\\//i,
|
|
914
|
+
/%2e%2e%5c/i, // encoded ..\\
|
|
915
|
+
];
|
|
916
|
+
|
|
917
|
+
const url = req.url + JSON.stringify(req.params);
|
|
918
|
+
|
|
919
|
+
for (const pattern of pathTraversalPatterns) {
|
|
920
|
+
if (pattern.test(url)) {
|
|
921
|
+
throw new BadRequestException('Potential path traversal attack detected');
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* CSRF Protection
|
|
929
|
+
*/
|
|
930
|
+
@Injectable()
|
|
931
|
+
export class CsrfMiddleware implements NestMiddleware {
|
|
932
|
+
private readonly tokenStore = new Map<string, string>();
|
|
933
|
+
|
|
934
|
+
use(req: Request, res: Response, next: NextFunction): void {
|
|
935
|
+
// Skip for safe methods
|
|
936
|
+
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
|
|
937
|
+
return next();
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const token = req.headers['x-csrf-token'] as string;
|
|
941
|
+
const sessionId = req.session?.id || req.headers['x-session-id'] as string;
|
|
942
|
+
|
|
943
|
+
if (!token || !sessionId) {
|
|
944
|
+
throw new BadRequestException('CSRF token missing');
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const storedToken = this.tokenStore.get(sessionId);
|
|
948
|
+
if (token !== storedToken) {
|
|
949
|
+
throw new BadRequestException('Invalid CSRF token');
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
next();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
generateToken(sessionId: string): string {
|
|
956
|
+
const token = require('crypto').randomBytes(32).toString('hex');
|
|
957
|
+
this.tokenStore.set(sessionId, token);
|
|
958
|
+
return token;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Request size limiter
|
|
964
|
+
*/
|
|
965
|
+
export function createSizeLimiter(maxSize: number) {
|
|
966
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
967
|
+
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
|
|
968
|
+
if (contentLength > maxSize) {
|
|
969
|
+
throw new BadRequestException(\`Request too large. Max size: \${maxSize} bytes\`);
|
|
970
|
+
}
|
|
971
|
+
next();
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
`;
|
|
975
|
+
}
|
|
976
|
+
function generateSecretVault() {
|
|
977
|
+
return `/**
|
|
978
|
+
* Secret Vault Service
|
|
979
|
+
* Secure secret management
|
|
980
|
+
*/
|
|
981
|
+
|
|
982
|
+
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
983
|
+
import * as crypto from 'crypto';
|
|
984
|
+
|
|
985
|
+
export interface Secret {
|
|
986
|
+
key: string;
|
|
987
|
+
value: string;
|
|
988
|
+
version: number;
|
|
989
|
+
createdAt: Date;
|
|
990
|
+
expiresAt?: Date;
|
|
991
|
+
metadata?: Record<string, any>;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
@Injectable()
|
|
995
|
+
export class SecretVaultService implements OnModuleInit {
|
|
996
|
+
private readonly logger = new Logger(SecretVaultService.name);
|
|
997
|
+
private readonly secrets = new Map<string, Secret[]>();
|
|
998
|
+
private masterKey: Buffer | null = null;
|
|
999
|
+
|
|
1000
|
+
async onModuleInit() {
|
|
1001
|
+
// Initialize master key from environment
|
|
1002
|
+
const masterKeyHex = process.env.VAULT_MASTER_KEY;
|
|
1003
|
+
if (masterKeyHex) {
|
|
1004
|
+
this.masterKey = Buffer.from(masterKeyHex, 'hex');
|
|
1005
|
+
this.logger.log('Secret vault initialized with master key');
|
|
1006
|
+
} else {
|
|
1007
|
+
this.logger.warn('No master key configured, secrets will be stored in plaintext');
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Store a secret
|
|
1013
|
+
*/
|
|
1014
|
+
async setSecret(key: string, value: string, options?: { expiresIn?: number; metadata?: Record<string, any> }): Promise<Secret> {
|
|
1015
|
+
const versions = this.secrets.get(key) || [];
|
|
1016
|
+
const version = versions.length + 1;
|
|
1017
|
+
|
|
1018
|
+
const encryptedValue = this.masterKey ? this.encrypt(value) : value;
|
|
1019
|
+
|
|
1020
|
+
const secret: Secret = {
|
|
1021
|
+
key,
|
|
1022
|
+
value: encryptedValue,
|
|
1023
|
+
version,
|
|
1024
|
+
createdAt: new Date(),
|
|
1025
|
+
expiresAt: options?.expiresIn ? new Date(Date.now() + options.expiresIn) : undefined,
|
|
1026
|
+
metadata: options?.metadata,
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
versions.push(secret);
|
|
1030
|
+
this.secrets.set(key, versions);
|
|
1031
|
+
|
|
1032
|
+
this.logger.log(\`Secret '\${key}' stored (version \${version})\`);
|
|
1033
|
+
return { ...secret, value: '[REDACTED]' };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Get a secret
|
|
1038
|
+
*/
|
|
1039
|
+
async getSecret(key: string, version?: number): Promise<string | null> {
|
|
1040
|
+
const versions = this.secrets.get(key);
|
|
1041
|
+
if (!versions || versions.length === 0) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const secret = version
|
|
1046
|
+
? versions.find(s => s.version === version)
|
|
1047
|
+
: versions[versions.length - 1];
|
|
1048
|
+
|
|
1049
|
+
if (!secret) {
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (secret.expiresAt && new Date() > secret.expiresAt) {
|
|
1054
|
+
this.logger.warn(\`Secret '\${key}' has expired\`);
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return this.masterKey ? this.decrypt(secret.value) : secret.value;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Delete a secret
|
|
1063
|
+
*/
|
|
1064
|
+
async deleteSecret(key: string): Promise<boolean> {
|
|
1065
|
+
const deleted = this.secrets.delete(key);
|
|
1066
|
+
if (deleted) {
|
|
1067
|
+
this.logger.log(\`Secret '\${key}' deleted\`);
|
|
1068
|
+
}
|
|
1069
|
+
return deleted;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Rotate a secret
|
|
1074
|
+
*/
|
|
1075
|
+
async rotateSecret(key: string, newValue: string): Promise<Secret> {
|
|
1076
|
+
return this.setSecret(key, newValue);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* List secret keys
|
|
1081
|
+
*/
|
|
1082
|
+
listKeys(): string[] {
|
|
1083
|
+
return Array.from(this.secrets.keys());
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Get secret metadata
|
|
1088
|
+
*/
|
|
1089
|
+
getSecretInfo(key: string): Omit<Secret, 'value'> | null {
|
|
1090
|
+
const versions = this.secrets.get(key);
|
|
1091
|
+
if (!versions || versions.length === 0) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const latest = versions[versions.length - 1];
|
|
1096
|
+
return {
|
|
1097
|
+
key: latest.key,
|
|
1098
|
+
version: latest.version,
|
|
1099
|
+
createdAt: latest.createdAt,
|
|
1100
|
+
expiresAt: latest.expiresAt,
|
|
1101
|
+
metadata: latest.metadata,
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
private encrypt(plaintext: string): string {
|
|
1106
|
+
if (!this.masterKey) return plaintext;
|
|
1107
|
+
|
|
1108
|
+
const iv = crypto.randomBytes(16);
|
|
1109
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', this.masterKey, iv);
|
|
1110
|
+
|
|
1111
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|
1112
|
+
encrypted += cipher.final('hex');
|
|
1113
|
+
|
|
1114
|
+
const tag = cipher.getAuthTag();
|
|
1115
|
+
|
|
1116
|
+
return \`\${iv.toString('hex')}:\${tag.toString('hex')}:\${encrypted}\`;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
private decrypt(ciphertext: string): string {
|
|
1120
|
+
if (!this.masterKey) return ciphertext;
|
|
1121
|
+
|
|
1122
|
+
const [ivHex, tagHex, encrypted] = ciphertext.split(':');
|
|
1123
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
1124
|
+
const tag = Buffer.from(tagHex, 'hex');
|
|
1125
|
+
|
|
1126
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', this.masterKey, iv);
|
|
1127
|
+
decipher.setAuthTag(tag);
|
|
1128
|
+
|
|
1129
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
1130
|
+
decrypted += decipher.final('utf8');
|
|
1131
|
+
|
|
1132
|
+
return decrypted;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Environment secret loader
|
|
1138
|
+
*/
|
|
1139
|
+
export function loadSecrets(vault: SecretVaultService): Record<string, string> {
|
|
1140
|
+
const secrets: Record<string, string> = {};
|
|
1141
|
+
|
|
1142
|
+
for (const key of vault.listKeys()) {
|
|
1143
|
+
// Sync version - for initialization only
|
|
1144
|
+
const value = (vault as any).secrets.get(key)?.[0]?.value;
|
|
1145
|
+
if (value) {
|
|
1146
|
+
secrets[key] = value;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
return secrets;
|
|
1151
|
+
}
|
|
1152
|
+
`;
|
|
1153
|
+
}
|
|
1154
|
+
function generateInputSanitizer() {
|
|
1155
|
+
return `/**
|
|
1156
|
+
* Input Sanitizer
|
|
1157
|
+
* Clean and validate user input
|
|
1158
|
+
* OWASP A03:2021 compliant
|
|
1159
|
+
*/
|
|
1160
|
+
|
|
1161
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
1162
|
+
|
|
1163
|
+
@Injectable()
|
|
1164
|
+
export class InputSanitizer {
|
|
1165
|
+
private readonly logger = new Logger(InputSanitizer.name);
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Sanitize string for HTML output - prevents XSS
|
|
1169
|
+
* Includes complete entity encoding for all dangerous characters
|
|
1170
|
+
*/
|
|
1171
|
+
sanitizeHtml(input: string): string {
|
|
1172
|
+
if (!input || typeof input !== 'string') return '';
|
|
1173
|
+
|
|
1174
|
+
const map: Record<string, string> = {
|
|
1175
|
+
'&': '&',
|
|
1176
|
+
'<': '<',
|
|
1177
|
+
'>': '>',
|
|
1178
|
+
'"': '"',
|
|
1179
|
+
"'": ''',
|
|
1180
|
+
'/': '/',
|
|
1181
|
+
'\`': '`', // Backtick for template literals
|
|
1182
|
+
'=': '=', // Equals sign
|
|
1183
|
+
};
|
|
1184
|
+
return input.replace(/[&<>"'/\`=]/g, char => map[char] || char);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* @deprecated NEVER use this for SQL injection prevention!
|
|
1189
|
+
* Always use parameterized queries/prepared statements instead.
|
|
1190
|
+
* This method only exists for legacy compatibility.
|
|
1191
|
+
*/
|
|
1192
|
+
sanitizeSql(input: string): string {
|
|
1193
|
+
this.logger.warn('sanitizeSql is deprecated! Use parameterized queries instead.');
|
|
1194
|
+
// This is NOT safe - always use parameterized queries
|
|
1195
|
+
return input.replace(/['";\\\\]/g, '');
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Sanitize for shell commands using WHITELIST approach
|
|
1200
|
+
* Only allows alphanumeric, hyphen, underscore, and dot
|
|
1201
|
+
* For complex shell operations, prefer using spawn with args array
|
|
1202
|
+
*/
|
|
1203
|
+
sanitizeShell(input: string): string {
|
|
1204
|
+
if (!input || typeof input !== 'string') return '';
|
|
1205
|
+
|
|
1206
|
+
// Strict whitelist - only allow safe characters
|
|
1207
|
+
const sanitized = input.replace(/[^a-zA-Z0-9_\\-.]/g, '');
|
|
1208
|
+
|
|
1209
|
+
// Check for path traversal
|
|
1210
|
+
if (sanitized.includes('..')) {
|
|
1211
|
+
this.logger.warn(\`Shell sanitization blocked path traversal attempt: \${input}\`);
|
|
1212
|
+
return sanitized.replace(/\\.\\.+/g, '');
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return sanitized;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Sanitize filename - prevents path traversal and invalid chars
|
|
1220
|
+
*/
|
|
1221
|
+
sanitizeFilename(input: string): string {
|
|
1222
|
+
if (!input || typeof input !== 'string') return '';
|
|
1223
|
+
|
|
1224
|
+
return input
|
|
1225
|
+
.replace(/\\.\\./g, '') // Remove path traversal
|
|
1226
|
+
.replace(/[/\\\\]/g, '') // Remove path separators
|
|
1227
|
+
.replace(/[?%*:|"<>\\x00-\\x1f]/g, '-') // Remove invalid chars
|
|
1228
|
+
.replace(/^[-_.]+|[-_.]+$/g, '') // Trim leading/trailing special chars
|
|
1229
|
+
.substring(0, 255); // Limit length
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Sanitize URL - validates protocol and structure
|
|
1234
|
+
*/
|
|
1235
|
+
sanitizeUrl(input: string): string {
|
|
1236
|
+
if (!input || typeof input !== 'string') return '';
|
|
1237
|
+
|
|
1238
|
+
try {
|
|
1239
|
+
const url = new URL(input);
|
|
1240
|
+
|
|
1241
|
+
// Only allow http/https protocols
|
|
1242
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
1243
|
+
this.logger.warn(\`URL sanitization blocked invalid protocol: \${url.protocol}\`);
|
|
1244
|
+
return '';
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Block javascript: and data: URIs (case-insensitive)
|
|
1248
|
+
if (/^(javascript|data|vbscript):/i.test(input)) {
|
|
1249
|
+
this.logger.warn(\`URL sanitization blocked dangerous scheme: \${input}\`);
|
|
1250
|
+
return '';
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
return url.toString();
|
|
1254
|
+
} catch {
|
|
1255
|
+
return '';
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Strip all HTML tags - for plain text extraction
|
|
1261
|
+
*/
|
|
1262
|
+
stripHtml(input: string): string {
|
|
1263
|
+
if (!input || typeof input !== 'string') return '';
|
|
1264
|
+
|
|
1265
|
+
// Remove script and style content entirely
|
|
1266
|
+
let result = input
|
|
1267
|
+
.replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')
|
|
1268
|
+
.replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '');
|
|
1269
|
+
|
|
1270
|
+
// Then strip remaining tags
|
|
1271
|
+
result = result.replace(/<[^>]*>/g, '');
|
|
1272
|
+
|
|
1273
|
+
// Decode entities
|
|
1274
|
+
const entities: Record<string, string> = {
|
|
1275
|
+
'&': '&',
|
|
1276
|
+
'<': '<',
|
|
1277
|
+
'>': '>',
|
|
1278
|
+
'"': '"',
|
|
1279
|
+
''': "'",
|
|
1280
|
+
' ': ' ',
|
|
1281
|
+
};
|
|
1282
|
+
for (const [entity, char] of Object.entries(entities)) {
|
|
1283
|
+
result = result.replace(new RegExp(entity, 'g'), char);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
return result;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Normalize whitespace
|
|
1291
|
+
*/
|
|
1292
|
+
normalizeWhitespace(input: string): string {
|
|
1293
|
+
if (!input || typeof input !== 'string') return '';
|
|
1294
|
+
return input.replace(/[\\s\\u200B-\\u200D\\uFEFF]+/g, ' ').trim();
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Sanitize JSON - prevents XSS in JSON values and prototype pollution
|
|
1299
|
+
*/
|
|
1300
|
+
sanitizeJson(input: any, depth = 0): any {
|
|
1301
|
+
// Prevent infinite recursion
|
|
1302
|
+
if (depth > 10) {
|
|
1303
|
+
this.logger.warn('JSON sanitization max depth exceeded');
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
if (typeof input === 'string') {
|
|
1308
|
+
return this.sanitizeHtml(input);
|
|
1309
|
+
}
|
|
1310
|
+
if (Array.isArray(input)) {
|
|
1311
|
+
return input.map(item => this.sanitizeJson(item, depth + 1));
|
|
1312
|
+
}
|
|
1313
|
+
if (typeof input === 'object' && input !== null) {
|
|
1314
|
+
const result: Record<string, any> = {};
|
|
1315
|
+
for (const [key, value] of Object.entries(input)) {
|
|
1316
|
+
// Block prototype pollution attacks
|
|
1317
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
1318
|
+
this.logger.warn(\`JSON sanitization blocked prototype pollution key: \${key}\`);
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
result[this.sanitizeHtml(key)] = this.sanitizeJson(value, depth + 1);
|
|
1322
|
+
}
|
|
1323
|
+
return result;
|
|
1324
|
+
}
|
|
1325
|
+
return input;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Validate and sanitize email
|
|
1330
|
+
*/
|
|
1331
|
+
sanitizeEmail(input: string): string | null {
|
|
1332
|
+
if (!input || typeof input !== 'string') return null;
|
|
1333
|
+
|
|
1334
|
+
const email = input.toLowerCase().trim();
|
|
1335
|
+
|
|
1336
|
+
// RFC 5321 max length
|
|
1337
|
+
if (email.length > 254) return null;
|
|
1338
|
+
|
|
1339
|
+
// Basic email regex (more permissive to allow valid emails)
|
|
1340
|
+
const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
|
|
1341
|
+
return emailRegex.test(email) ? email : null;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Sanitize phone number
|
|
1346
|
+
*/
|
|
1347
|
+
sanitizePhone(input: string): string {
|
|
1348
|
+
if (!input || typeof input !== 'string') return '';
|
|
1349
|
+
const sanitized = input.replace(/[^0-9+\\-() ]/g, '');
|
|
1350
|
+
// E.164 max length is 15 digits plus '+'
|
|
1351
|
+
return sanitized.substring(0, 20);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Validate field name for database queries
|
|
1356
|
+
* Prevents SQL injection via dynamic column names
|
|
1357
|
+
*/
|
|
1358
|
+
validateFieldName(fieldName: string, allowedFields: string[]): string {
|
|
1359
|
+
if (!fieldName || typeof fieldName !== 'string') {
|
|
1360
|
+
throw new Error('Invalid field name');
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Must be in allowed list
|
|
1364
|
+
if (!allowedFields.includes(fieldName)) {
|
|
1365
|
+
throw new Error(\`Field "\${fieldName}" not allowed. Allowed: \${allowedFields.join(', ')}\`);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Additional safety check - must match safe pattern
|
|
1369
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(fieldName)) {
|
|
1370
|
+
throw new Error(\`Field name format invalid: \${fieldName}\`);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
return fieldName;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Sanitize decorator
|
|
1379
|
+
*/
|
|
1380
|
+
export function Sanitize(type: 'html' | 'sql' | 'filename' | 'url'): PropertyDecorator {
|
|
1381
|
+
return function (target: Object, propertyKey: string | symbol) {
|
|
1382
|
+
Reflect.defineMetadata('sanitize', type, target, propertyKey);
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
`;
|
|
1386
|
+
}
|
|
1387
|
+
function generateCorsConfig() {
|
|
1388
|
+
return `/**
|
|
1389
|
+
* Secure CORS Configuration
|
|
1390
|
+
* OWASP A05:2021 - Security Misconfiguration prevention
|
|
1391
|
+
*/
|
|
1392
|
+
|
|
1393
|
+
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
|
|
1394
|
+
import { Logger } from '@nestjs/common';
|
|
1395
|
+
|
|
1396
|
+
const logger = new Logger('CorsConfig');
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Create secure CORS configuration
|
|
1400
|
+
* - Strict origin validation (no wildcards in production)
|
|
1401
|
+
* - Explicit allowed methods
|
|
1402
|
+
* - Credentials handling with origin check
|
|
1403
|
+
*/
|
|
1404
|
+
export function createSecureCorsConfig(options: {
|
|
1405
|
+
allowedOrigins: string[];
|
|
1406
|
+
isProduction?: boolean;
|
|
1407
|
+
}): CorsOptions {
|
|
1408
|
+
const { allowedOrigins, isProduction = process.env.NODE_ENV === 'production' } = options;
|
|
1409
|
+
|
|
1410
|
+
// Validate origins
|
|
1411
|
+
if (isProduction && allowedOrigins.includes('*')) {
|
|
1412
|
+
throw new Error('CORS: Wildcard origin (*) not allowed in production');
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Normalize origins to prevent bypass (e.g., trailing slashes)
|
|
1416
|
+
const normalizedOrigins = allowedOrigins.map(origin => {
|
|
1417
|
+
if (origin === '*') return origin;
|
|
1418
|
+
return origin.replace(/\\/+$/, '').toLowerCase();
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
return {
|
|
1422
|
+
origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
|
|
1423
|
+
// Allow requests with no origin (like mobile apps or Postman)
|
|
1424
|
+
// In production, you may want to be stricter
|
|
1425
|
+
if (!origin) {
|
|
1426
|
+
if (isProduction) {
|
|
1427
|
+
logger.warn('Request with no origin header - consider blocking in production');
|
|
1428
|
+
}
|
|
1429
|
+
callback(null, true);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const normalizedOrigin = origin.replace(/\\/+$/, '').toLowerCase();
|
|
1434
|
+
|
|
1435
|
+
// Check against whitelist
|
|
1436
|
+
if (normalizedOrigins.includes('*') || normalizedOrigins.includes(normalizedOrigin)) {
|
|
1437
|
+
callback(null, true);
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Check for subdomain matching (if pattern like *.example.com is allowed)
|
|
1442
|
+
for (const allowed of normalizedOrigins) {
|
|
1443
|
+
if (allowed.startsWith('*.')) {
|
|
1444
|
+
const domain = allowed.slice(2);
|
|
1445
|
+
if (normalizedOrigin.endsWith(domain)) {
|
|
1446
|
+
callback(null, true);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
logger.warn(\`CORS blocked origin: \${origin}\`);
|
|
1453
|
+
callback(new Error('Not allowed by CORS'));
|
|
1454
|
+
},
|
|
1455
|
+
|
|
1456
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
1457
|
+
|
|
1458
|
+
// Only expose safe headers
|
|
1459
|
+
allowedHeaders: [
|
|
1460
|
+
'Content-Type',
|
|
1461
|
+
'Authorization',
|
|
1462
|
+
'X-Requested-With',
|
|
1463
|
+
'Accept',
|
|
1464
|
+
'Origin',
|
|
1465
|
+
'X-CSRF-Token',
|
|
1466
|
+
],
|
|
1467
|
+
|
|
1468
|
+
// Credentials require explicit origin (not *)
|
|
1469
|
+
credentials: true,
|
|
1470
|
+
|
|
1471
|
+
// Cache preflight for 24 hours
|
|
1472
|
+
maxAge: 86400,
|
|
1473
|
+
|
|
1474
|
+
// Limit exposed headers to prevent information leak
|
|
1475
|
+
exposedHeaders: ['X-Request-Id', 'X-RateLimit-Remaining'],
|
|
1476
|
+
|
|
1477
|
+
// Don't allow OPTIONS to continue to route handlers
|
|
1478
|
+
preflightContinue: false,
|
|
1479
|
+
|
|
1480
|
+
// Return 204 for OPTIONS
|
|
1481
|
+
optionsSuccessStatus: 204,
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* Validate origin against allowed list
|
|
1487
|
+
* Use for manual CORS checks
|
|
1488
|
+
*/
|
|
1489
|
+
export function isOriginAllowed(origin: string, allowedOrigins: string[]): boolean {
|
|
1490
|
+
if (!origin) return false;
|
|
1491
|
+
|
|
1492
|
+
const normalizedOrigin = origin.replace(/\\/+$/, '').toLowerCase();
|
|
1493
|
+
|
|
1494
|
+
for (const allowed of allowedOrigins) {
|
|
1495
|
+
const normalizedAllowed = allowed.replace(/\\/+$/, '').toLowerCase();
|
|
1496
|
+
|
|
1497
|
+
if (normalizedAllowed === '*') return true;
|
|
1498
|
+
if (normalizedAllowed === normalizedOrigin) return true;
|
|
1499
|
+
|
|
1500
|
+
// Subdomain matching
|
|
1501
|
+
if (normalizedAllowed.startsWith('*.')) {
|
|
1502
|
+
const domain = normalizedAllowed.slice(2);
|
|
1503
|
+
if (normalizedOrigin.endsWith('.' + domain) || normalizedOrigin === domain) {
|
|
1504
|
+
return true;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return false;
|
|
1510
|
+
}
|
|
1511
|
+
`;
|
|
1512
|
+
}
|
|
1513
|
+
function generateCookieConfig() {
|
|
1514
|
+
return `/**
|
|
1515
|
+
* Secure Cookie Configuration
|
|
1516
|
+
* OWASP A07:2021 - Authentication best practices
|
|
1517
|
+
*/
|
|
1518
|
+
|
|
1519
|
+
import { CookieOptions } from 'express';
|
|
1520
|
+
import { Logger } from '@nestjs/common';
|
|
1521
|
+
|
|
1522
|
+
const logger = new Logger('CookieConfig');
|
|
1523
|
+
|
|
1524
|
+
export interface SecureCookieOptions {
|
|
1525
|
+
name: string;
|
|
1526
|
+
isProduction?: boolean;
|
|
1527
|
+
domain?: string;
|
|
1528
|
+
maxAgeSeconds?: number;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* Create secure cookie options
|
|
1533
|
+
* - HttpOnly to prevent XSS access
|
|
1534
|
+
* - Secure flag for HTTPS-only
|
|
1535
|
+
* - SameSite to prevent CSRF
|
|
1536
|
+
* - Proper path and domain scoping
|
|
1537
|
+
*/
|
|
1538
|
+
export function createSecureCookieOptions(options: SecureCookieOptions): CookieOptions {
|
|
1539
|
+
const {
|
|
1540
|
+
isProduction = process.env.NODE_ENV === 'production',
|
|
1541
|
+
domain,
|
|
1542
|
+
maxAgeSeconds = 3600, // 1 hour default
|
|
1543
|
+
} = options;
|
|
1544
|
+
|
|
1545
|
+
return {
|
|
1546
|
+
// Prevent JavaScript access (XSS protection)
|
|
1547
|
+
httpOnly: true,
|
|
1548
|
+
|
|
1549
|
+
// Only send over HTTPS in production
|
|
1550
|
+
secure: isProduction,
|
|
1551
|
+
|
|
1552
|
+
// Strict SameSite prevents CSRF
|
|
1553
|
+
// Use 'lax' if you need cross-site GET requests (e.g., OAuth redirects)
|
|
1554
|
+
sameSite: 'strict',
|
|
1555
|
+
|
|
1556
|
+
// Scope to specific path
|
|
1557
|
+
path: '/',
|
|
1558
|
+
|
|
1559
|
+
// Domain scoping (omit to use current domain only)
|
|
1560
|
+
domain: domain,
|
|
1561
|
+
|
|
1562
|
+
// Set explicit expiry
|
|
1563
|
+
maxAge: maxAgeSeconds * 1000, // Express uses milliseconds
|
|
1564
|
+
|
|
1565
|
+
// Signed cookies for integrity (requires cookie-parser with secret)
|
|
1566
|
+
signed: true,
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* Session cookie configuration
|
|
1572
|
+
*/
|
|
1573
|
+
export function createSessionCookieOptions(options: {
|
|
1574
|
+
isProduction?: boolean;
|
|
1575
|
+
sessionMaxAgeHours?: number;
|
|
1576
|
+
}): CookieOptions {
|
|
1577
|
+
const {
|
|
1578
|
+
isProduction = process.env.NODE_ENV === 'production',
|
|
1579
|
+
sessionMaxAgeHours = 24,
|
|
1580
|
+
} = options;
|
|
1581
|
+
|
|
1582
|
+
return {
|
|
1583
|
+
httpOnly: true,
|
|
1584
|
+
secure: isProduction,
|
|
1585
|
+
sameSite: 'strict',
|
|
1586
|
+
path: '/',
|
|
1587
|
+
maxAge: sessionMaxAgeHours * 60 * 60 * 1000,
|
|
1588
|
+
signed: true,
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* Authentication token cookie options
|
|
1594
|
+
* More restrictive for auth tokens
|
|
1595
|
+
*/
|
|
1596
|
+
export function createAuthCookieOptions(options: {
|
|
1597
|
+
isProduction?: boolean;
|
|
1598
|
+
tokenMaxAgeMinutes?: number;
|
|
1599
|
+
domain?: string;
|
|
1600
|
+
}): CookieOptions {
|
|
1601
|
+
const {
|
|
1602
|
+
isProduction = process.env.NODE_ENV === 'production',
|
|
1603
|
+
tokenMaxAgeMinutes = 15, // Short-lived for security
|
|
1604
|
+
domain,
|
|
1605
|
+
} = options;
|
|
1606
|
+
|
|
1607
|
+
return {
|
|
1608
|
+
httpOnly: true,
|
|
1609
|
+
secure: isProduction,
|
|
1610
|
+
sameSite: 'strict',
|
|
1611
|
+
path: '/',
|
|
1612
|
+
domain,
|
|
1613
|
+
maxAge: tokenMaxAgeMinutes * 60 * 1000,
|
|
1614
|
+
signed: true,
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
/**
|
|
1619
|
+
* Refresh token cookie options
|
|
1620
|
+
* Longer lived but more restricted
|
|
1621
|
+
*/
|
|
1622
|
+
export function createRefreshTokenCookieOptions(options: {
|
|
1623
|
+
isProduction?: boolean;
|
|
1624
|
+
maxAgeDays?: number;
|
|
1625
|
+
domain?: string;
|
|
1626
|
+
}): CookieOptions {
|
|
1627
|
+
const {
|
|
1628
|
+
isProduction = process.env.NODE_ENV === 'production',
|
|
1629
|
+
maxAgeDays = 7,
|
|
1630
|
+
domain,
|
|
1631
|
+
} = options;
|
|
1632
|
+
|
|
1633
|
+
return {
|
|
1634
|
+
httpOnly: true,
|
|
1635
|
+
secure: isProduction,
|
|
1636
|
+
sameSite: 'strict',
|
|
1637
|
+
// Restrict refresh token to specific path
|
|
1638
|
+
path: '/auth/refresh',
|
|
1639
|
+
domain,
|
|
1640
|
+
maxAge: maxAgeDays * 24 * 60 * 60 * 1000,
|
|
1641
|
+
signed: true,
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* Clear cookie securely
|
|
1647
|
+
*/
|
|
1648
|
+
export function clearCookieOptions(isProduction: boolean = process.env.NODE_ENV === 'production'): CookieOptions {
|
|
1649
|
+
return {
|
|
1650
|
+
httpOnly: true,
|
|
1651
|
+
secure: isProduction,
|
|
1652
|
+
sameSite: 'strict',
|
|
1653
|
+
path: '/',
|
|
1654
|
+
maxAge: 0,
|
|
1655
|
+
expires: new Date(0),
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/**
|
|
1660
|
+
* Cookie name constants with prefix for clarity
|
|
1661
|
+
*/
|
|
1662
|
+
export const CookieNames = {
|
|
1663
|
+
SESSION: '__Host-session', // __Host- prefix requires Secure and Path=/
|
|
1664
|
+
ACCESS_TOKEN: '__Host-access',
|
|
1665
|
+
REFRESH_TOKEN: '__Host-refresh',
|
|
1666
|
+
CSRF: '__Host-csrf',
|
|
1667
|
+
} as const;
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* Validate cookie name follows security best practices
|
|
1671
|
+
* __Host- prefix ensures Secure, no Domain, Path=/
|
|
1672
|
+
* __Secure- prefix ensures Secure
|
|
1673
|
+
*/
|
|
1674
|
+
export function validateCookieName(name: string, isProduction: boolean): void {
|
|
1675
|
+
if (isProduction) {
|
|
1676
|
+
if (name.startsWith('__Host-') || name.startsWith('__Secure-')) {
|
|
1677
|
+
return; // Valid secure prefix
|
|
1678
|
+
}
|
|
1679
|
+
logger.warn(\`Cookie "\${name}" should use __Host- or __Secure- prefix in production\`);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
`;
|
|
1683
|
+
}
|
|
1684
|
+
function generateJwtSecurity() {
|
|
1685
|
+
return `/**
|
|
1686
|
+
* JWT Security Service
|
|
1687
|
+
* OWASP A07:2021 - Identification and Authentication Failures prevention
|
|
1688
|
+
*/
|
|
1689
|
+
|
|
1690
|
+
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
|
1691
|
+
import * as crypto from 'crypto';
|
|
1692
|
+
|
|
1693
|
+
export interface JwtHeader {
|
|
1694
|
+
alg: string;
|
|
1695
|
+
typ: string;
|
|
1696
|
+
kid?: string;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
export interface JwtPayload {
|
|
1700
|
+
iss?: string; // Issuer
|
|
1701
|
+
sub?: string; // Subject
|
|
1702
|
+
aud?: string | string[]; // Audience
|
|
1703
|
+
exp?: number; // Expiration
|
|
1704
|
+
nbf?: number; // Not Before
|
|
1705
|
+
iat?: number; // Issued At
|
|
1706
|
+
jti?: string; // JWT ID (for revocation)
|
|
1707
|
+
[key: string]: any;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
export interface JwtValidationOptions {
|
|
1711
|
+
issuer?: string;
|
|
1712
|
+
audience?: string | string[];
|
|
1713
|
+
algorithms?: string[];
|
|
1714
|
+
clockTolerance?: number;
|
|
1715
|
+
maxAge?: number;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Secure algorithm whitelist - block 'none' and weak algorithms
|
|
1719
|
+
const ALLOWED_ALGORITHMS = ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'];
|
|
1720
|
+
const WEAK_ALGORITHMS = ['none', 'HS1', 'RS1'];
|
|
1721
|
+
|
|
1722
|
+
@Injectable()
|
|
1723
|
+
export class JwtSecurityService {
|
|
1724
|
+
private readonly logger = new Logger(JwtSecurityService.name);
|
|
1725
|
+
private readonly revokedTokens = new Set<string>();
|
|
1726
|
+
private readonly usedJtis = new Map<string, number>(); // jti -> expiry timestamp
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* Validate JWT structure and claims
|
|
1730
|
+
* Checks for common JWT attacks:
|
|
1731
|
+
* - Algorithm confusion (none attack, weak algorithms)
|
|
1732
|
+
* - Token replay (jti tracking)
|
|
1733
|
+
* - Expired/not-yet-valid tokens
|
|
1734
|
+
* - Issuer/audience mismatch
|
|
1735
|
+
*/
|
|
1736
|
+
validateToken(token: string, options: JwtValidationOptions = {}): { header: JwtHeader; payload: JwtPayload } {
|
|
1737
|
+
if (!token || typeof token !== 'string') {
|
|
1738
|
+
throw new UnauthorizedException('Token is required');
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// Split and decode
|
|
1742
|
+
const parts = token.split('.');
|
|
1743
|
+
if (parts.length !== 3) {
|
|
1744
|
+
throw new UnauthorizedException('Invalid token format');
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
let header: JwtHeader;
|
|
1748
|
+
let payload: JwtPayload;
|
|
1749
|
+
|
|
1750
|
+
try {
|
|
1751
|
+
header = JSON.parse(this.base64UrlDecode(parts[0]));
|
|
1752
|
+
payload = JSON.parse(this.base64UrlDecode(parts[1]));
|
|
1753
|
+
} catch {
|
|
1754
|
+
throw new UnauthorizedException('Invalid token encoding');
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// Validate algorithm
|
|
1758
|
+
this.validateAlgorithm(header.alg, options.algorithms);
|
|
1759
|
+
|
|
1760
|
+
// Validate claims
|
|
1761
|
+
this.validateClaims(payload, options);
|
|
1762
|
+
|
|
1763
|
+
// Check if token is revoked
|
|
1764
|
+
if (payload.jti && this.revokedTokens.has(payload.jti)) {
|
|
1765
|
+
throw new UnauthorizedException('Token has been revoked');
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// Check for replay (same jti used before expiry)
|
|
1769
|
+
if (payload.jti) {
|
|
1770
|
+
const existingExpiry = this.usedJtis.get(payload.jti);
|
|
1771
|
+
if (existingExpiry && Date.now() < existingExpiry) {
|
|
1772
|
+
// Potential replay - same jti, not expired
|
|
1773
|
+
this.logger.warn(\`Potential JWT replay detected: jti=\${payload.jti}\`);
|
|
1774
|
+
// Note: In some cases, legitimate retries may reuse tokens
|
|
1775
|
+
// Consider your use case before throwing here
|
|
1776
|
+
}
|
|
1777
|
+
if (payload.exp) {
|
|
1778
|
+
this.usedJtis.set(payload.jti, payload.exp * 1000);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
return { header, payload };
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
/**
|
|
1786
|
+
* Validate algorithm is allowed
|
|
1787
|
+
* Prevents algorithm confusion attacks
|
|
1788
|
+
*/
|
|
1789
|
+
private validateAlgorithm(alg: string, allowedAlgorithms?: string[]): void {
|
|
1790
|
+
// Check against weak algorithms
|
|
1791
|
+
if (WEAK_ALGORITHMS.includes(alg.toLowerCase())) {
|
|
1792
|
+
this.logger.error(\`JWT with weak/dangerous algorithm detected: \${alg}\`);
|
|
1793
|
+
throw new UnauthorizedException('Invalid token algorithm');
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// Check against allowed list
|
|
1797
|
+
const allowed = allowedAlgorithms?.length ? allowedAlgorithms : ALLOWED_ALGORITHMS;
|
|
1798
|
+
if (!allowed.includes(alg)) {
|
|
1799
|
+
throw new UnauthorizedException('Token algorithm not allowed');
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Validate JWT claims
|
|
1805
|
+
*/
|
|
1806
|
+
private validateClaims(payload: JwtPayload, options: JwtValidationOptions): void {
|
|
1807
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1808
|
+
const clockTolerance = options.clockTolerance || 30; // 30 seconds tolerance
|
|
1809
|
+
|
|
1810
|
+
// Check expiration
|
|
1811
|
+
if (payload.exp !== undefined) {
|
|
1812
|
+
if (now > payload.exp + clockTolerance) {
|
|
1813
|
+
throw new UnauthorizedException('Token has expired');
|
|
1814
|
+
}
|
|
1815
|
+
} else {
|
|
1816
|
+
// Tokens without expiration are risky
|
|
1817
|
+
this.logger.warn('JWT without expiration claim');
|
|
1818
|
+
if (options.maxAge) {
|
|
1819
|
+
throw new UnauthorizedException('Token missing required exp claim');
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Check not before
|
|
1824
|
+
if (payload.nbf !== undefined) {
|
|
1825
|
+
if (now < payload.nbf - clockTolerance) {
|
|
1826
|
+
throw new UnauthorizedException('Token not yet valid');
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Check issued at (with max age)
|
|
1831
|
+
if (options.maxAge && payload.iat !== undefined) {
|
|
1832
|
+
if (now - payload.iat > options.maxAge) {
|
|
1833
|
+
throw new UnauthorizedException('Token exceeds maximum age');
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// Check issuer
|
|
1838
|
+
if (options.issuer && payload.iss !== options.issuer) {
|
|
1839
|
+
throw new UnauthorizedException('Invalid token issuer');
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// Check audience
|
|
1843
|
+
if (options.audience) {
|
|
1844
|
+
const expectedAudiences = Array.isArray(options.audience) ? options.audience : [options.audience];
|
|
1845
|
+
const tokenAudiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
|
|
1846
|
+
|
|
1847
|
+
const hasValidAudience = expectedAudiences.some(aud => tokenAudiences.includes(aud));
|
|
1848
|
+
if (!hasValidAudience) {
|
|
1849
|
+
throw new UnauthorizedException('Invalid token audience');
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* Revoke a token by jti
|
|
1856
|
+
*/
|
|
1857
|
+
revokeToken(jti: string): void {
|
|
1858
|
+
this.revokedTokens.add(jti);
|
|
1859
|
+
this.logger.log(\`Token revoked: jti=\${jti}\`);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/**
|
|
1863
|
+
* Generate a secure, unique JWT ID
|
|
1864
|
+
*/
|
|
1865
|
+
generateJti(): string {
|
|
1866
|
+
return crypto.randomUUID();
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Clean up expired jti tracking
|
|
1871
|
+
* Call periodically to prevent memory bloat
|
|
1872
|
+
*/
|
|
1873
|
+
cleanupExpiredJtis(): void {
|
|
1874
|
+
const now = Date.now();
|
|
1875
|
+
let cleaned = 0;
|
|
1876
|
+
|
|
1877
|
+
for (const [jti, expiry] of this.usedJtis.entries()) {
|
|
1878
|
+
if (now > expiry) {
|
|
1879
|
+
this.usedJtis.delete(jti);
|
|
1880
|
+
this.revokedTokens.delete(jti);
|
|
1881
|
+
cleaned++;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
if (cleaned > 0) {
|
|
1886
|
+
this.logger.debug(\`Cleaned up \${cleaned} expired JWT IDs\`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
/**
|
|
1891
|
+
* Create secure token payload with required claims
|
|
1892
|
+
*/
|
|
1893
|
+
createSecurePayload(options: {
|
|
1894
|
+
subject: string;
|
|
1895
|
+
issuer: string;
|
|
1896
|
+
audience: string | string[];
|
|
1897
|
+
expiresInSeconds?: number;
|
|
1898
|
+
data?: Record<string, any>;
|
|
1899
|
+
}): JwtPayload {
|
|
1900
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1901
|
+
const expiresIn = options.expiresInSeconds || 900; // 15 minutes default
|
|
1902
|
+
|
|
1903
|
+
return {
|
|
1904
|
+
sub: options.subject,
|
|
1905
|
+
iss: options.issuer,
|
|
1906
|
+
aud: options.audience,
|
|
1907
|
+
iat: now,
|
|
1908
|
+
nbf: now,
|
|
1909
|
+
exp: now + expiresIn,
|
|
1910
|
+
jti: this.generateJti(),
|
|
1911
|
+
...options.data,
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
private base64UrlDecode(str: string): string {
|
|
1916
|
+
// Add padding if needed
|
|
1917
|
+
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
1918
|
+
while (base64.length % 4) {
|
|
1919
|
+
base64 += '=';
|
|
1920
|
+
}
|
|
1921
|
+
return Buffer.from(base64, 'base64').toString('utf-8');
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/**
|
|
1926
|
+
* Secure JWT configuration recommendations
|
|
1927
|
+
*/
|
|
1928
|
+
export const JwtSecurityRecommendations = {
|
|
1929
|
+
// Use RS256 or ES256 in production (asymmetric)
|
|
1930
|
+
RECOMMENDED_ALGORITHM: 'RS256',
|
|
1931
|
+
|
|
1932
|
+
// Short token lifetime (15 min for access, longer for refresh)
|
|
1933
|
+
ACCESS_TOKEN_LIFETIME: 15 * 60, // 15 minutes
|
|
1934
|
+
REFRESH_TOKEN_LIFETIME: 7 * 24 * 60 * 60, // 7 days
|
|
1935
|
+
|
|
1936
|
+
// Always include these claims
|
|
1937
|
+
REQUIRED_CLAIMS: ['iss', 'sub', 'aud', 'exp', 'iat', 'jti'],
|
|
1938
|
+
|
|
1939
|
+
// Key rotation recommendation
|
|
1940
|
+
KEY_ROTATION_DAYS: 90,
|
|
1941
|
+
};
|
|
1942
|
+
`;
|
|
1943
|
+
}
|
|
1944
|
+
function generateSecurityHeadersConfig() {
|
|
1945
|
+
return `/**
|
|
1946
|
+
* Comprehensive Security Headers Configuration
|
|
1947
|
+
* OWASP A05:2021 - Security Misconfiguration prevention
|
|
1948
|
+
*/
|
|
1949
|
+
|
|
1950
|
+
import { Response } from 'express';
|
|
1951
|
+
|
|
1952
|
+
export interface SecurityHeadersOptions {
|
|
1953
|
+
isProduction?: boolean;
|
|
1954
|
+
contentSecurityPolicy?: ContentSecurityPolicyOptions;
|
|
1955
|
+
permissionsPolicy?: PermissionsPolicyOptions;
|
|
1956
|
+
reportUri?: string;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
export interface ContentSecurityPolicyOptions {
|
|
1960
|
+
defaultSrc?: string[];
|
|
1961
|
+
scriptSrc?: string[];
|
|
1962
|
+
styleSrc?: string[];
|
|
1963
|
+
imgSrc?: string[];
|
|
1964
|
+
fontSrc?: string[];
|
|
1965
|
+
connectSrc?: string[];
|
|
1966
|
+
frameSrc?: string[];
|
|
1967
|
+
objectSrc?: string[];
|
|
1968
|
+
mediaSrc?: string[];
|
|
1969
|
+
workerSrc?: string[];
|
|
1970
|
+
frameAncestors?: string[];
|
|
1971
|
+
formAction?: string[];
|
|
1972
|
+
baseUri?: string[];
|
|
1973
|
+
upgradeInsecureRequests?: boolean;
|
|
1974
|
+
blockAllMixedContent?: boolean;
|
|
1975
|
+
reportUri?: string;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
export interface PermissionsPolicyOptions {
|
|
1979
|
+
accelerometer?: string;
|
|
1980
|
+
camera?: string;
|
|
1981
|
+
geolocation?: string;
|
|
1982
|
+
gyroscope?: string;
|
|
1983
|
+
magnetometer?: string;
|
|
1984
|
+
microphone?: string;
|
|
1985
|
+
payment?: string;
|
|
1986
|
+
usb?: string;
|
|
1987
|
+
fullscreen?: string;
|
|
1988
|
+
[key: string]: string | undefined;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/**
|
|
1992
|
+
* Set all security headers on a response
|
|
1993
|
+
*/
|
|
1994
|
+
export function setSecurityHeaders(res: Response, options: SecurityHeadersOptions = {}): void {
|
|
1995
|
+
const isProduction = options.isProduction ?? process.env.NODE_ENV === 'production';
|
|
1996
|
+
|
|
1997
|
+
// Clickjacking protection
|
|
1998
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
1999
|
+
|
|
2000
|
+
// XSS protection (legacy but still useful)
|
|
2001
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
2002
|
+
|
|
2003
|
+
// Prevent MIME type sniffing
|
|
2004
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
2005
|
+
|
|
2006
|
+
// HTTPS strict transport
|
|
2007
|
+
if (isProduction) {
|
|
2008
|
+
// 2 years, include subdomains, allow preloading
|
|
2009
|
+
res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// Referrer policy - strict for security, relaxed for internal analytics
|
|
2013
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
2014
|
+
|
|
2015
|
+
// Content Security Policy
|
|
2016
|
+
const csp = buildContentSecurityPolicy(options.contentSecurityPolicy);
|
|
2017
|
+
if (isProduction) {
|
|
2018
|
+
res.setHeader('Content-Security-Policy', csp);
|
|
2019
|
+
} else {
|
|
2020
|
+
// Report-only in development for debugging
|
|
2021
|
+
res.setHeader('Content-Security-Policy-Report-Only', csp);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// Permissions Policy (formerly Feature-Policy)
|
|
2025
|
+
res.setHeader('Permissions-Policy', buildPermissionsPolicy(options.permissionsPolicy));
|
|
2026
|
+
|
|
2027
|
+
// Cross-Origin policies
|
|
2028
|
+
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
2029
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
|
|
2030
|
+
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
2031
|
+
|
|
2032
|
+
// Prevent Adobe products from cross-domain requests
|
|
2033
|
+
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
|
|
2034
|
+
|
|
2035
|
+
// Prevent caching of sensitive data
|
|
2036
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
2037
|
+
res.setHeader('Pragma', 'no-cache');
|
|
2038
|
+
res.setHeader('Expires', '0');
|
|
2039
|
+
res.setHeader('Surrogate-Control', 'no-store');
|
|
2040
|
+
|
|
2041
|
+
// Clear site data on logout (set this header on logout endpoint)
|
|
2042
|
+
// res.setHeader('Clear-Site-Data', '"cache", "cookies", "storage"');
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
/**
|
|
2046
|
+
* Build Content Security Policy header value
|
|
2047
|
+
*/
|
|
2048
|
+
function buildContentSecurityPolicy(options: ContentSecurityPolicyOptions = {}): string {
|
|
2049
|
+
const directives: string[] = [];
|
|
2050
|
+
|
|
2051
|
+
const addDirective = (name: string, values: string[] | undefined, defaultValue: string[]) => {
|
|
2052
|
+
const finalValues = values ?? defaultValue;
|
|
2053
|
+
if (finalValues.length > 0) {
|
|
2054
|
+
directives.push(\`\${name} \${finalValues.join(' ')}\`);
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
|
|
2058
|
+
// Restrictive defaults
|
|
2059
|
+
addDirective('default-src', options.defaultSrc, ["'self'"]);
|
|
2060
|
+
addDirective('script-src', options.scriptSrc, ["'self'"]);
|
|
2061
|
+
addDirective('style-src', options.styleSrc, ["'self'"]);
|
|
2062
|
+
addDirective('img-src', options.imgSrc, ["'self'", 'data:', 'https:']);
|
|
2063
|
+
addDirective('font-src', options.fontSrc, ["'self'"]);
|
|
2064
|
+
addDirective('connect-src', options.connectSrc, ["'self'"]);
|
|
2065
|
+
addDirective('frame-src', options.frameSrc, ["'none'"]);
|
|
2066
|
+
addDirective('object-src', options.objectSrc, ["'none'"]);
|
|
2067
|
+
addDirective('media-src', options.mediaSrc, ["'self'"]);
|
|
2068
|
+
addDirective('worker-src', options.workerSrc, ["'self'"]);
|
|
2069
|
+
addDirective('frame-ancestors', options.frameAncestors, ["'none'"]);
|
|
2070
|
+
addDirective('form-action', options.formAction, ["'self'"]);
|
|
2071
|
+
addDirective('base-uri', options.baseUri, ["'self'"]);
|
|
2072
|
+
|
|
2073
|
+
if (options.upgradeInsecureRequests !== false) {
|
|
2074
|
+
directives.push('upgrade-insecure-requests');
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
if (options.blockAllMixedContent !== false) {
|
|
2078
|
+
directives.push('block-all-mixed-content');
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
if (options.reportUri) {
|
|
2082
|
+
directives.push(\`report-uri \${options.reportUri}\`);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
return directives.join('; ');
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
/**
|
|
2089
|
+
* Build Permissions Policy header value
|
|
2090
|
+
*/
|
|
2091
|
+
function buildPermissionsPolicy(options: PermissionsPolicyOptions = {}): string {
|
|
2092
|
+
const defaultPolicy: PermissionsPolicyOptions = {
|
|
2093
|
+
accelerometer: '()',
|
|
2094
|
+
camera: '()',
|
|
2095
|
+
geolocation: '()',
|
|
2096
|
+
gyroscope: '()',
|
|
2097
|
+
magnetometer: '()',
|
|
2098
|
+
microphone: '()',
|
|
2099
|
+
payment: '()',
|
|
2100
|
+
usb: '()',
|
|
2101
|
+
fullscreen: '(self)',
|
|
2102
|
+
...options,
|
|
2103
|
+
};
|
|
2104
|
+
|
|
2105
|
+
return Object.entries(defaultPolicy)
|
|
2106
|
+
.filter(([_, value]) => value !== undefined)
|
|
2107
|
+
.map(([key, value]) => \`\${key}=\${value}\`)
|
|
2108
|
+
.join(', ');
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
/**
|
|
2112
|
+
* Security headers for API responses (less restrictive CSP)
|
|
2113
|
+
*/
|
|
2114
|
+
export function setApiSecurityHeaders(res: Response, options: SecurityHeadersOptions = {}): void {
|
|
2115
|
+
const isProduction = options.isProduction ?? process.env.NODE_ENV === 'production';
|
|
2116
|
+
|
|
2117
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
2118
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
2119
|
+
|
|
2120
|
+
if (isProduction) {
|
|
2121
|
+
res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains');
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// API-specific: prevent caching by default
|
|
2125
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
2126
|
+
res.setHeader('Pragma', 'no-cache');
|
|
2127
|
+
|
|
2128
|
+
// Prevent embedding
|
|
2129
|
+
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
/**
|
|
2133
|
+
* Headers to set on logout for session cleanup
|
|
2134
|
+
*/
|
|
2135
|
+
export function setLogoutHeaders(res: Response): void {
|
|
2136
|
+
// Clear all site data
|
|
2137
|
+
res.setHeader('Clear-Site-Data', '"cache", "cookies", "storage"');
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
/**
|
|
2141
|
+
* Security headers middleware factory
|
|
2142
|
+
*/
|
|
2143
|
+
export function securityHeadersMiddleware(options: SecurityHeadersOptions = {}) {
|
|
2144
|
+
return (req: any, res: Response, next: () => void) => {
|
|
2145
|
+
setSecurityHeaders(res, options);
|
|
2146
|
+
next();
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
/**
|
|
2151
|
+
* API security headers middleware factory
|
|
2152
|
+
*/
|
|
2153
|
+
export function apiSecurityHeadersMiddleware(options: SecurityHeadersOptions = {}) {
|
|
2154
|
+
return (req: any, res: Response, next: () => void) => {
|
|
2155
|
+
setApiSecurityHeaders(res, options);
|
|
2156
|
+
next();
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
`;
|
|
2160
|
+
}
|
|
2161
|
+
function generateSecurityModule() {
|
|
2162
|
+
return `import { Module, Global, DynamicModule, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
2163
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
2164
|
+
import { EncryptionService } from './encryption.service';
|
|
2165
|
+
import { RbacService, AbacService } from './rbac.service';
|
|
2166
|
+
import { RolesGuard, PermissionsGuard } from './rbac.decorator';
|
|
2167
|
+
import { OwaspMiddleware, CsrfMiddleware } from './owasp.middleware';
|
|
2168
|
+
import { SecretVaultService } from './secret-vault.service';
|
|
2169
|
+
import { InputSanitizer } from './input-sanitizer';
|
|
2170
|
+
import { JwtSecurityService } from './jwt.security';
|
|
2171
|
+
import { securityHeadersMiddleware } from './security-headers.config';
|
|
2172
|
+
|
|
2173
|
+
export interface SecurityModuleOptions {
|
|
2174
|
+
enableRbac?: boolean;
|
|
2175
|
+
enableOwasp?: boolean;
|
|
2176
|
+
enableCsrf?: boolean;
|
|
2177
|
+
enableSecurityHeaders?: boolean;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
@Global()
|
|
2181
|
+
@Module({})
|
|
2182
|
+
export class SecurityModule implements NestModule {
|
|
2183
|
+
static options: SecurityModuleOptions = {};
|
|
2184
|
+
|
|
2185
|
+
static forRoot(options: SecurityModuleOptions = {}): DynamicModule {
|
|
2186
|
+
this.options = options;
|
|
2187
|
+
|
|
2188
|
+
const providers: any[] = [
|
|
2189
|
+
EncryptionService,
|
|
2190
|
+
RbacService,
|
|
2191
|
+
AbacService,
|
|
2192
|
+
SecretVaultService,
|
|
2193
|
+
InputSanitizer,
|
|
2194
|
+
JwtSecurityService,
|
|
2195
|
+
CsrfMiddleware,
|
|
2196
|
+
];
|
|
2197
|
+
|
|
2198
|
+
if (options.enableRbac !== false) {
|
|
2199
|
+
providers.push(
|
|
2200
|
+
{ provide: APP_GUARD, useClass: RolesGuard },
|
|
2201
|
+
{ provide: APP_GUARD, useClass: PermissionsGuard },
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
return {
|
|
2206
|
+
module: SecurityModule,
|
|
2207
|
+
providers,
|
|
2208
|
+
exports: [
|
|
2209
|
+
EncryptionService,
|
|
2210
|
+
RbacService,
|
|
2211
|
+
AbacService,
|
|
2212
|
+
SecretVaultService,
|
|
2213
|
+
InputSanitizer,
|
|
2214
|
+
JwtSecurityService,
|
|
2215
|
+
],
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
configure(consumer: MiddlewareConsumer) {
|
|
2220
|
+
if (SecurityModule.options.enableOwasp !== false) {
|
|
2221
|
+
consumer.apply(OwaspMiddleware).forRoutes('*');
|
|
2222
|
+
}
|
|
2223
|
+
if (SecurityModule.options.enableCsrf) {
|
|
2224
|
+
consumer.apply(CsrfMiddleware).forRoutes('*');
|
|
2225
|
+
}
|
|
2226
|
+
if (SecurityModule.options.enableSecurityHeaders !== false) {
|
|
2227
|
+
consumer.apply(securityHeadersMiddleware()).forRoutes('*');
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
`;
|
|
2232
|
+
}
|
|
2233
|
+
//# sourceMappingURL=security-patterns.js.map
|