Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
35
marketing-agent/services/compliance-guard/Dockerfile
Normal file
35
marketing-agent/services/compliance-guard/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
# Install system dependencies
|
||||
RUN apk add --no-cache python3 py3-pip make g++
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install --production
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create nodejs user and group
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S nodejs -u 1001
|
||||
|
||||
# Create logs directory with proper permissions
|
||||
RUN mkdir -p logs && chown -R nodejs:nodejs logs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3006
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD node health-check.js || exit 1
|
||||
|
||||
# Switch to non-root user
|
||||
USER nodejs
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "src/app.js"]
|
||||
59
marketing-agent/services/compliance-guard/package.json
Normal file
59
marketing-agent/services/compliance-guard/package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "compliance-guard-service",
|
||||
"version": "1.0.0",
|
||||
"description": "Compliance and regulatory service for Telegram Marketing Intelligence Agent",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "nodemon src/index.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src",
|
||||
"format": "prettier --write src"
|
||||
},
|
||||
"keywords": [
|
||||
"compliance",
|
||||
"regulatory",
|
||||
"privacy",
|
||||
"gdpr",
|
||||
"ccpa",
|
||||
"telegram"
|
||||
],
|
||||
"author": "Marketing Agent Team",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"mongoose": "^7.5.0",
|
||||
"redis": "^4.6.5",
|
||||
"amqplib": "^0.10.3",
|
||||
"axios": "^1.5.0",
|
||||
"joi": "^17.10.0",
|
||||
"uuid": "^9.0.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"rate-limiter-flexible": "^2.4.2",
|
||||
"geoip-lite": "^1.4.7",
|
||||
"node-cron": "^3.0.2",
|
||||
"prom-client": "^14.2.0",
|
||||
"winston": "^3.10.0",
|
||||
"dotenv": "^16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^6.3.3",
|
||||
"eslint": "^8.49.0",
|
||||
"prettier": "^3.0.3",
|
||||
"@types/jest": "^29.5.5"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"coverageDirectory": "coverage",
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js",
|
||||
"!src/index.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
1
marketing-agent/services/compliance-guard/src/app.js
Normal file
1
marketing-agent/services/compliance-guard/src/app.js
Normal file
@@ -0,0 +1 @@
|
||||
import './index.js';
|
||||
106
marketing-agent/services/compliance-guard/src/config/index.js
Normal file
106
marketing-agent/services/compliance-guard/src/config/index.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const config = {
|
||||
port: process.env.PORT || 3006,
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
|
||||
mongodb: {
|
||||
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/compliance',
|
||||
options: {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
}
|
||||
},
|
||||
|
||||
redis: {
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379',
|
||||
prefix: process.env.REDIS_PREFIX || 'compliance:',
|
||||
ttl: parseInt(process.env.REDIS_TTL || '3600')
|
||||
},
|
||||
|
||||
rabbitmq: {
|
||||
url: process.env.RABBITMQ_URL || 'amqp://localhost:5672',
|
||||
exchange: process.env.RABBITMQ_EXCHANGE || 'compliance',
|
||||
queues: {
|
||||
violations: 'compliance.violations',
|
||||
audits: 'compliance.audits',
|
||||
consents: 'compliance.consents',
|
||||
requests: 'compliance.requests'
|
||||
}
|
||||
},
|
||||
|
||||
compliance: {
|
||||
regions: {
|
||||
gdpr: ['EU', 'UK', 'EEA'],
|
||||
ccpa: ['US-CA'],
|
||||
lgpd: ['BR'],
|
||||
pipeda: ['CA'],
|
||||
appi: ['JP']
|
||||
},
|
||||
retentionPeriods: {
|
||||
default: 365, // days
|
||||
eu: 365,
|
||||
us: 730,
|
||||
marketing: 180,
|
||||
analytics: 365
|
||||
},
|
||||
consentTypes: [
|
||||
'marketing',
|
||||
'analytics',
|
||||
'personalization',
|
||||
'data_sharing',
|
||||
'cookies'
|
||||
]
|
||||
},
|
||||
|
||||
encryption: {
|
||||
algorithm: process.env.ENCRYPTION_ALGORITHM || 'aes-256-gcm',
|
||||
keyDerivation: process.env.KEY_DERIVATION || 'pbkdf2',
|
||||
saltRounds: parseInt(process.env.SALT_ROUNDS || '10'),
|
||||
encryptionKey: process.env.ENCRYPTION_KEY || 'your-encryption-key-here'
|
||||
},
|
||||
|
||||
rateLimit: {
|
||||
dataRequests: {
|
||||
points: 10,
|
||||
duration: 3600, // 1 hour
|
||||
blockDuration: 3600
|
||||
},
|
||||
consentUpdates: {
|
||||
points: 50,
|
||||
duration: 86400, // 24 hours
|
||||
blockDuration: 3600
|
||||
}
|
||||
},
|
||||
|
||||
monitoring: {
|
||||
checkInterval: parseInt(process.env.CHECK_INTERVAL || '300000'), // 5 minutes
|
||||
violationThreshold: parseInt(process.env.VIOLATION_THRESHOLD || '5'),
|
||||
alertChannels: process.env.ALERT_CHANNELS?.split(',') || ['email', 'slack']
|
||||
},
|
||||
|
||||
metrics: {
|
||||
port: parseInt(process.env.METRICS_PORT || '9106'),
|
||||
prefix: process.env.METRICS_PREFIX || 'compliance_guard_'
|
||||
},
|
||||
|
||||
logging: {
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
file: process.env.LOG_FILE || 'logs/compliance.log',
|
||||
auditFile: process.env.AUDIT_LOG_FILE || 'logs/audit.log'
|
||||
},
|
||||
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || 'your-compliance-secret',
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '24h'
|
||||
},
|
||||
|
||||
notifications: {
|
||||
email: {
|
||||
from: process.env.EMAIL_FROM || 'compliance@example.com',
|
||||
dpo: process.env.DPO_EMAIL || 'dpo@example.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
104
marketing-agent/services/compliance-guard/src/index.js
Normal file
104
marketing-agent/services/compliance-guard/src/index.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import express from 'express';
|
||||
import mongoose from 'mongoose';
|
||||
import Redis from 'redis';
|
||||
import { config } from './config/index.js';
|
||||
import { logger } from './utils/logger.js';
|
||||
import { setupRoutes } from './routes/index.js';
|
||||
import { connectRabbitMQ } from './services/messaging.js';
|
||||
import { startMetricsServer } from './utils/metrics.js';
|
||||
import { ComplianceMonitor } from './services/complianceMonitor.js';
|
||||
import { DataProtectionService } from './services/dataProtection.js';
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
service: 'compliance-guard',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Setup routes
|
||||
setupRoutes(app);
|
||||
|
||||
// Error handling
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error('Unhandled error:', err);
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: err.message
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize services
|
||||
async function initializeServices() {
|
||||
try {
|
||||
// Connect to MongoDB
|
||||
await mongoose.connect(config.mongodb.uri, config.mongodb.options);
|
||||
logger.info('Connected to MongoDB');
|
||||
|
||||
// Connect to Redis
|
||||
const redisClient = Redis.createClient({ url: config.redis.url });
|
||||
await redisClient.connect();
|
||||
logger.info('Connected to Redis');
|
||||
|
||||
// Store Redis client globally
|
||||
app.locals.redis = redisClient;
|
||||
|
||||
// Connect to RabbitMQ
|
||||
const channel = await connectRabbitMQ();
|
||||
app.locals.rabbitmq = channel;
|
||||
|
||||
// Initialize compliance monitor
|
||||
const complianceMonitor = new ComplianceMonitor(redisClient);
|
||||
await complianceMonitor.start();
|
||||
app.locals.complianceMonitor = complianceMonitor;
|
||||
|
||||
// Initialize data protection service
|
||||
const dataProtection = new DataProtectionService();
|
||||
app.locals.dataProtection = dataProtection;
|
||||
|
||||
// Start metrics server
|
||||
startMetricsServer(config.metrics.port);
|
||||
|
||||
// Start main server
|
||||
app.listen(config.port, () => {
|
||||
logger.info(`Compliance Guard service listening on port ${config.port}`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize services:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', async () => {
|
||||
logger.info('SIGTERM received, shutting down gracefully');
|
||||
|
||||
try {
|
||||
if (app.locals.complianceMonitor) {
|
||||
await app.locals.complianceMonitor.stop();
|
||||
}
|
||||
|
||||
if (app.locals.redis) {
|
||||
await app.locals.redis.quit();
|
||||
}
|
||||
|
||||
await mongoose.connection.close();
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('Error during shutdown:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Start the service
|
||||
initializeServices();
|
||||
@@ -0,0 +1,148 @@
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { metrics } from '../utils/metrics.js';
|
||||
|
||||
/**
|
||||
* Error handling middleware
|
||||
*/
|
||||
export const errorHandler = (err, req, res, next) => {
|
||||
// Log error
|
||||
logger.error('Error occurred:', {
|
||||
error: {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
code: err.code,
|
||||
statusCode: err.statusCode
|
||||
},
|
||||
request: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
ip: req.ip,
|
||||
userId: req.user?.id
|
||||
}
|
||||
});
|
||||
|
||||
// Record error metric
|
||||
metrics.recordApiCall(req.route?.path || 'unknown', 'error');
|
||||
|
||||
// Determine status code
|
||||
const statusCode = err.statusCode || err.status || 500;
|
||||
|
||||
// Prepare error response
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
error: {
|
||||
message: err.message || 'Internal server error',
|
||||
code: err.code || 'INTERNAL_ERROR'
|
||||
}
|
||||
};
|
||||
|
||||
// Add additional error details in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
errorResponse.error.stack = err.stack;
|
||||
errorResponse.error.details = err.details;
|
||||
}
|
||||
|
||||
// Handle specific error types
|
||||
if (err.name === 'ValidationError') {
|
||||
errorResponse.error.code = 'VALIDATION_ERROR';
|
||||
errorResponse.error.details = err.details || [];
|
||||
return res.status(400).json(errorResponse);
|
||||
}
|
||||
|
||||
if (err.name === 'MongoError' && err.code === 11000) {
|
||||
errorResponse.error.code = 'DUPLICATE_ENTRY';
|
||||
errorResponse.error.message = 'Duplicate entry found';
|
||||
return res.status(409).json(errorResponse);
|
||||
}
|
||||
|
||||
if (err.name === 'CastError') {
|
||||
errorResponse.error.code = 'INVALID_ID';
|
||||
errorResponse.error.message = 'Invalid ID format';
|
||||
return res.status(400).json(errorResponse);
|
||||
}
|
||||
|
||||
// Default error response
|
||||
res.status(statusCode).json(errorResponse);
|
||||
};
|
||||
|
||||
/**
|
||||
* Async error wrapper
|
||||
*/
|
||||
export const asyncHandler = (fn) => {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Not found middleware
|
||||
*/
|
||||
export const notFound = (req, res, next) => {
|
||||
const error = new Error(`Not found - ${req.originalUrl}`);
|
||||
error.statusCode = 404;
|
||||
next(error);
|
||||
};
|
||||
|
||||
/**
|
||||
* Request validation middleware
|
||||
*/
|
||||
export const validateRequest = (schema) => {
|
||||
return (req, res, next) => {
|
||||
const { error } = schema.validate(req.body, {
|
||||
abortEarly: false,
|
||||
stripUnknown: true
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const validationError = new Error('Validation failed');
|
||||
validationError.name = 'ValidationError';
|
||||
validationError.details = error.details.map(detail => ({
|
||||
field: detail.path.join('.'),
|
||||
message: detail.message
|
||||
}));
|
||||
return next(validationError);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Rate limit error handler
|
||||
*/
|
||||
export const rateLimitHandler = (req, res) => {
|
||||
logger.warn('Rate limit exceeded:', {
|
||||
ip: req.ip,
|
||||
userId: req.user?.id,
|
||||
endpoint: req.path
|
||||
});
|
||||
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Too many requests',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
retryAfter: res.getHeader('Retry-After')
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Timeout middleware
|
||||
*/
|
||||
export const timeout = (seconds = 30) => {
|
||||
return (req, res, next) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
const error = new Error('Request timeout');
|
||||
error.statusCode = 408;
|
||||
error.code = 'REQUEST_TIMEOUT';
|
||||
next(error);
|
||||
}, seconds * 1000);
|
||||
|
||||
res.on('finish', () => {
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
235
marketing-agent/services/compliance-guard/src/models/AuditLog.js
Normal file
235
marketing-agent/services/compliance-guard/src/models/AuditLog.js
Normal file
@@ -0,0 +1,235 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const auditLogSchema = new mongoose.Schema({
|
||||
// Multi-tenant support
|
||||
tenantId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Tenant',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
auditId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true
|
||||
},
|
||||
accountId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
index: true
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['consent', 'data_access', 'data_modification', 'data_deletion', 'security', 'configuration', 'compliance'],
|
||||
index: true
|
||||
},
|
||||
resource: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
resourceId: {
|
||||
type: String,
|
||||
index: true
|
||||
},
|
||||
details: {
|
||||
before: mongoose.Schema.Types.Mixed,
|
||||
after: mongoose.Schema.Types.Mixed,
|
||||
changes: [String],
|
||||
reason: String,
|
||||
metadata: mongoose.Schema.Types.Mixed
|
||||
},
|
||||
result: {
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['success', 'failure', 'partial'],
|
||||
required: true
|
||||
},
|
||||
error: String,
|
||||
affectedRecords: Number
|
||||
},
|
||||
actor: {
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['user', 'admin', 'system', 'api'],
|
||||
required: true
|
||||
},
|
||||
id: String,
|
||||
name: String,
|
||||
role: String,
|
||||
ip: String,
|
||||
userAgent: String,
|
||||
location: {
|
||||
country: String,
|
||||
region: String,
|
||||
city: String
|
||||
}
|
||||
},
|
||||
compliance: {
|
||||
regulation: String,
|
||||
legalBasis: String,
|
||||
retention: {
|
||||
days: {
|
||||
type: Number,
|
||||
default: 2555 // 7 years default
|
||||
},
|
||||
expiryDate: Date
|
||||
}
|
||||
},
|
||||
security: {
|
||||
encrypted: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
signed: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
signature: String,
|
||||
hash: String
|
||||
},
|
||||
timestamp: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
required: true,
|
||||
index: true
|
||||
}
|
||||
}, {
|
||||
timestamps: false // We use custom timestamp field
|
||||
});
|
||||
|
||||
// Indexes
|
||||
auditLogSchema.index({ accountId: 1, timestamp: -1 });
|
||||
auditLogSchema.index({ userId: 1, category: 1, timestamp: -1 });
|
||||
auditLogSchema.index({ 'compliance.retention.expiryDate': 1 }, { sparse: true });
|
||||
|
||||
// Multi-tenant indexes
|
||||
auditLogSchema.index({ tenantId: 1, accountId: 1, timestamp: -1 });
|
||||
auditLogSchema.index({ tenantId: 1, userId: 1, category: 1, timestamp: -1 });
|
||||
auditLogSchema.index({ tenantId: 1, 'compliance.retention.expiryDate': 1 }, { sparse: true });
|
||||
|
||||
// Make audit logs immutable
|
||||
auditLogSchema.pre('save', function(next) {
|
||||
if (!this.isNew) {
|
||||
return next(new Error('Audit logs cannot be modified'));
|
||||
}
|
||||
|
||||
// Calculate retention expiry
|
||||
if (this.compliance.retention.days) {
|
||||
const expiryDate = new Date(this.timestamp);
|
||||
expiryDate.setDate(expiryDate.getDate() + this.compliance.retention.days);
|
||||
this.compliance.retention.expiryDate = expiryDate;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Prevent updates and deletes
|
||||
auditLogSchema.pre(['update', 'updateOne', 'updateMany', 'findOneAndUpdate'], function() {
|
||||
throw new Error('Audit logs cannot be updated');
|
||||
});
|
||||
|
||||
auditLogSchema.pre(['remove', 'deleteOne', 'deleteMany', 'findOneAndDelete'], function() {
|
||||
throw new Error('Audit logs cannot be deleted manually');
|
||||
});
|
||||
|
||||
// Methods
|
||||
auditLogSchema.methods.isExpired = function() {
|
||||
return this.compliance.retention.expiryDate &&
|
||||
this.compliance.retention.expiryDate < new Date();
|
||||
};
|
||||
|
||||
// Statics
|
||||
auditLogSchema.statics.log = async function(data) {
|
||||
const audit = new this({
|
||||
...data,
|
||||
auditId: `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
});
|
||||
|
||||
return await audit.save();
|
||||
};
|
||||
|
||||
auditLogSchema.statics.search = function(criteria) {
|
||||
const query = {};
|
||||
|
||||
if (criteria.accountId) query.accountId = criteria.accountId;
|
||||
if (criteria.userId) query.userId = criteria.userId;
|
||||
if (criteria.category) query.category = criteria.category;
|
||||
if (criteria.action) query.action = new RegExp(criteria.action, 'i');
|
||||
if (criteria.resource) query.resource = criteria.resource;
|
||||
if (criteria.resourceId) query.resourceId = criteria.resourceId;
|
||||
|
||||
if (criteria.startDate || criteria.endDate) {
|
||||
query.timestamp = {};
|
||||
if (criteria.startDate) query.timestamp.$gte = criteria.startDate;
|
||||
if (criteria.endDate) query.timestamp.$lte = criteria.endDate;
|
||||
}
|
||||
|
||||
if (criteria.actorType) query['actor.type'] = criteria.actorType;
|
||||
if (criteria.actorId) query['actor.id'] = criteria.actorId;
|
||||
if (criteria.resultStatus) query['result.status'] = criteria.resultStatus;
|
||||
|
||||
return this.find(query).sort({ timestamp: -1 });
|
||||
};
|
||||
|
||||
auditLogSchema.statics.getActivitySummary = async function(accountId, period = 7) {
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - period);
|
||||
|
||||
return await this.aggregate([
|
||||
{
|
||||
$match: {
|
||||
accountId,
|
||||
timestamp: { $gte: startDate }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
date: { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } },
|
||||
category: '$category',
|
||||
status: '$result.status'
|
||||
},
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
date: '$_id.date',
|
||||
category: '$_id.category'
|
||||
},
|
||||
total: { $sum: '$count' },
|
||||
success: {
|
||||
$sum: { $cond: [{ $eq: ['$_id.status', 'success'] }, '$count', 0] }
|
||||
},
|
||||
failure: {
|
||||
$sum: { $cond: [{ $eq: ['$_id.status', 'failure'] }, '$count', 0] }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { '_id.date': -1, '_id.category': 1 }
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
auditLogSchema.statics.cleanupExpired = async function() {
|
||||
const result = await this.deleteMany({
|
||||
'compliance.retention.expiryDate': { $lt: new Date() }
|
||||
});
|
||||
|
||||
return result.deletedCount;
|
||||
};
|
||||
|
||||
export const AuditLog = mongoose.model('AuditLog', auditLogSchema);
|
||||
@@ -0,0 +1,299 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const violationSchema = new mongoose.Schema({
|
||||
// Multi-tenant support
|
||||
tenantId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Tenant',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
violationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
accountId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: [
|
||||
'data_retention',
|
||||
'consent_missing',
|
||||
'consent_expired',
|
||||
'data_access_denied',
|
||||
'processing_without_basis',
|
||||
'cross_border_transfer',
|
||||
'security_breach',
|
||||
'notification_failure',
|
||||
'age_verification',
|
||||
'opt_out_failure',
|
||||
'excessive_data_collection',
|
||||
'purpose_limitation',
|
||||
'third_party_sharing'
|
||||
]
|
||||
},
|
||||
severity: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['low', 'medium', 'high', 'critical'],
|
||||
default: 'medium'
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['detected', 'investigating', 'confirmed', 'remediated', 'closed', 'escalated'],
|
||||
default: 'detected'
|
||||
},
|
||||
details: {
|
||||
description: String,
|
||||
affectedUsers: Number,
|
||||
affectedData: [String],
|
||||
regulation: {
|
||||
type: String,
|
||||
enum: ['gdpr', 'ccpa', 'lgpd', 'pipeda', 'appi', 'other']
|
||||
},
|
||||
articles: [String], // Violated articles/sections
|
||||
detectionMethod: {
|
||||
type: String,
|
||||
enum: ['automated', 'manual', 'user_report', 'audit', 'external']
|
||||
},
|
||||
evidence: [{
|
||||
type: String,
|
||||
data: mongoose.Schema.Types.Mixed,
|
||||
timestamp: Date
|
||||
}]
|
||||
},
|
||||
timeline: {
|
||||
detectedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
investigationStarted: Date,
|
||||
confirmedAt: Date,
|
||||
remediationStarted: Date,
|
||||
remediationCompleted: Date,
|
||||
closedAt: Date,
|
||||
deadlines: {
|
||||
notification: Date,
|
||||
remediation: Date,
|
||||
reporting: Date
|
||||
}
|
||||
},
|
||||
remediation: {
|
||||
plan: String,
|
||||
actions: [{
|
||||
action: String,
|
||||
responsible: String,
|
||||
deadline: Date,
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'in_progress', 'completed', 'failed']
|
||||
},
|
||||
completedAt: Date,
|
||||
notes: String
|
||||
}],
|
||||
preventiveMeasures: [String],
|
||||
cost: {
|
||||
estimated: Number,
|
||||
actual: Number,
|
||||
currency: {
|
||||
type: String,
|
||||
default: 'USD'
|
||||
}
|
||||
}
|
||||
},
|
||||
notifications: {
|
||||
authorities: [{
|
||||
name: String,
|
||||
notifiedAt: Date,
|
||||
method: String,
|
||||
reference: String,
|
||||
response: String
|
||||
}],
|
||||
users: {
|
||||
required: Boolean,
|
||||
notifiedCount: Number,
|
||||
notificationMethod: String,
|
||||
notifiedAt: Date,
|
||||
template: String
|
||||
},
|
||||
internal: [{
|
||||
role: String,
|
||||
name: String,
|
||||
notifiedAt: Date
|
||||
}]
|
||||
},
|
||||
risk: {
|
||||
score: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
max: 10
|
||||
},
|
||||
likelihood: {
|
||||
type: String,
|
||||
enum: ['very_low', 'low', 'medium', 'high', 'very_high']
|
||||
},
|
||||
impact: {
|
||||
type: String,
|
||||
enum: ['minimal', 'low', 'moderate', 'high', 'severe']
|
||||
},
|
||||
financialExposure: {
|
||||
min: Number,
|
||||
max: Number,
|
||||
currency: {
|
||||
type: String,
|
||||
default: 'USD'
|
||||
}
|
||||
}
|
||||
},
|
||||
investigation: {
|
||||
assignedTo: String,
|
||||
team: [String],
|
||||
findings: String,
|
||||
rootCause: String,
|
||||
contributingFactors: [String],
|
||||
recommendations: [String]
|
||||
},
|
||||
compliance: {
|
||||
reportable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
reportingDeadline: Date,
|
||||
fineAmount: Number,
|
||||
appealStatus: String,
|
||||
correctionNotice: String
|
||||
},
|
||||
metadata: {
|
||||
source: {
|
||||
service: String,
|
||||
component: String,
|
||||
version: String
|
||||
},
|
||||
tags: [String],
|
||||
relatedViolations: [String],
|
||||
parentViolation: String
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes
|
||||
violationSchema.index({ accountId: 1, status: 1, severity: 1 });
|
||||
violationSchema.index({ 'timeline.detectedAt': -1 });
|
||||
violationSchema.index({ type: 1, status: 1 });
|
||||
violationSchema.index({ 'compliance.reportingDeadline': 1 }, { sparse: true });
|
||||
|
||||
// Multi-tenant indexes
|
||||
violationSchema.index({ tenantId: 1, accountId: 1, status: 1, severity: 1 });
|
||||
violationSchema.index({ tenantId: 1, 'timeline.detectedAt': -1 });
|
||||
violationSchema.index({ tenantId: 1, type: 1, status: 1 });
|
||||
violationSchema.index({ tenantId: 1, 'compliance.reportingDeadline': 1 }, { sparse: true });
|
||||
|
||||
// Methods
|
||||
violationSchema.methods.calculateRiskScore = function() {
|
||||
const severityScores = { low: 2, medium: 4, high: 7, critical: 10 };
|
||||
const impactScores = { minimal: 1, low: 2, moderate: 4, high: 7, severe: 10 };
|
||||
const likelihoodScores = { very_low: 1, low: 2, medium: 4, high: 7, very_high: 10 };
|
||||
|
||||
const severityScore = severityScores[this.severity] || 5;
|
||||
const impactScore = impactScores[this.risk.impact] || 5;
|
||||
const likelihoodScore = likelihoodScores[this.risk.likelihood] || 5;
|
||||
|
||||
// Weighted calculation
|
||||
this.risk.score = Math.min(10, (severityScore * 0.4 + impactScore * 0.4 + likelihoodScore * 0.2));
|
||||
|
||||
return this.risk.score;
|
||||
};
|
||||
|
||||
violationSchema.methods.isOverdue = function() {
|
||||
const now = new Date();
|
||||
|
||||
if (this.timeline.deadlines.notification && this.timeline.deadlines.notification < now &&
|
||||
(!this.notifications.authorities || this.notifications.authorities.length === 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.timeline.deadlines.remediation && this.timeline.deadlines.remediation < now &&
|
||||
this.status !== 'remediated' && this.status !== 'closed') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
violationSchema.methods.needsEscalation = function() {
|
||||
return this.severity === 'critical' ||
|
||||
this.risk.score > 8 ||
|
||||
this.isOverdue() ||
|
||||
this.details.affectedUsers > 1000;
|
||||
};
|
||||
|
||||
// Statics
|
||||
violationSchema.statics.findActive = function(accountId) {
|
||||
const query = {
|
||||
status: { $nin: ['closed', 'remediated'] }
|
||||
};
|
||||
|
||||
if (accountId) query.accountId = accountId;
|
||||
|
||||
return this.find(query).sort({ 'risk.score': -1, 'timeline.detectedAt': -1 });
|
||||
};
|
||||
|
||||
violationSchema.statics.findRequiringNotification = function() {
|
||||
return this.find({
|
||||
'compliance.reportable': true,
|
||||
'notifications.authorities': { $size: 0 },
|
||||
'timeline.deadlines.notification': { $exists: true }
|
||||
}).sort({ 'timeline.deadlines.notification': 1 });
|
||||
};
|
||||
|
||||
violationSchema.statics.getViolationStats = async function(accountId, period) {
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - (period || 30));
|
||||
|
||||
const match = {
|
||||
'timeline.detectedAt': { $gte: startDate }
|
||||
};
|
||||
|
||||
if (accountId) match.accountId = accountId;
|
||||
|
||||
return await this.aggregate([
|
||||
{ $match: match },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
type: '$type',
|
||||
severity: '$severity',
|
||||
status: '$status'
|
||||
},
|
||||
count: { $sum: 1 },
|
||||
avgRiskScore: { $avg: '$risk.score' },
|
||||
totalAffectedUsers: { $sum: '$details.affectedUsers' }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
total: { $sum: '$count' },
|
||||
byType: {
|
||||
$push: {
|
||||
type: '$_id.type',
|
||||
severity: '$_id.severity',
|
||||
status: '$_id.status',
|
||||
count: '$count'
|
||||
}
|
||||
},
|
||||
avgRiskScore: { $avg: '$avgRiskScore' },
|
||||
totalAffectedUsers: { $sum: '$totalAffectedUsers' }
|
||||
}
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
export const ComplianceViolation = mongoose.model('ComplianceViolation', violationSchema);
|
||||
@@ -0,0 +1,168 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const consentSchema = new mongoose.Schema({
|
||||
// Multi-tenant support
|
||||
tenantId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Tenant',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
consentId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
accountId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['marketing', 'analytics', 'personalization', 'data_sharing', 'cookies']
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['granted', 'denied', 'withdrawn'],
|
||||
default: 'denied'
|
||||
},
|
||||
scope: {
|
||||
type: String,
|
||||
enum: ['all', 'specific'],
|
||||
default: 'all'
|
||||
},
|
||||
specificScopes: [{
|
||||
category: String,
|
||||
granted: Boolean
|
||||
}],
|
||||
version: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'en'
|
||||
},
|
||||
method: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['explicit', 'implicit', 'opt-out']
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['web', 'app', 'api', 'import', 'manual']
|
||||
},
|
||||
metadata: {
|
||||
ip: String,
|
||||
userAgent: String,
|
||||
location: {
|
||||
country: String,
|
||||
region: String,
|
||||
city: String
|
||||
},
|
||||
referrer: String,
|
||||
sessionId: String
|
||||
},
|
||||
legalBasis: {
|
||||
type: String,
|
||||
enum: ['consent', 'contract', 'legal_obligation', 'vital_interests', 'public_task', 'legitimate_interests']
|
||||
},
|
||||
withdrawalReason: String,
|
||||
expiryDate: Date,
|
||||
renewable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
parentConsentId: String, // For consent updates/renewals
|
||||
audit: {
|
||||
createdBy: String,
|
||||
updatedBy: String,
|
||||
approvedBy: String,
|
||||
notes: String
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes
|
||||
consentSchema.index({ userId: 1, type: 1, createdAt: -1 });
|
||||
consentSchema.index({ accountId: 1, status: 1 });
|
||||
consentSchema.index({ expiryDate: 1 }, { sparse: true });
|
||||
|
||||
// Multi-tenant indexes
|
||||
consentSchema.index({ tenantId: 1, userId: 1, type: 1, createdAt: -1 });
|
||||
consentSchema.index({ tenantId: 1, accountId: 1, status: 1 });
|
||||
consentSchema.index({ tenantId: 1, expiryDate: 1 }, { sparse: true });
|
||||
|
||||
// Methods
|
||||
consentSchema.methods.isActive = function() {
|
||||
if (this.status !== 'granted') return false;
|
||||
if (this.expiryDate && this.expiryDate < new Date()) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
consentSchema.methods.needsRenewal = function(daysBeforeExpiry = 30) {
|
||||
if (!this.renewable || !this.expiryDate) return false;
|
||||
if (this.status !== 'granted') return false;
|
||||
|
||||
const expiryThreshold = new Date();
|
||||
expiryThreshold.setDate(expiryThreshold.getDate() + daysBeforeExpiry);
|
||||
|
||||
return this.expiryDate <= expiryThreshold;
|
||||
};
|
||||
|
||||
consentSchema.methods.withdraw = function(reason) {
|
||||
this.status = 'withdrawn';
|
||||
this.withdrawalReason = reason;
|
||||
this.updatedAt = new Date();
|
||||
};
|
||||
|
||||
// Statics
|
||||
consentSchema.statics.findActiveConsents = function(userId, types) {
|
||||
const query = {
|
||||
userId,
|
||||
status: 'granted',
|
||||
$or: [
|
||||
{ expiryDate: { $exists: false } },
|
||||
{ expiryDate: { $gt: new Date() } }
|
||||
]
|
||||
};
|
||||
|
||||
if (types && types.length > 0) {
|
||||
query.type = { $in: types };
|
||||
}
|
||||
|
||||
return this.find(query);
|
||||
};
|
||||
|
||||
consentSchema.statics.hasConsent = async function(userId, type) {
|
||||
const consent = await this.findOne({
|
||||
userId,
|
||||
type,
|
||||
status: 'granted',
|
||||
$or: [
|
||||
{ expiryDate: { $exists: false } },
|
||||
{ expiryDate: { $gt: new Date() } }
|
||||
]
|
||||
}).sort({ createdAt: -1 });
|
||||
|
||||
return consent?.isActive() || false;
|
||||
};
|
||||
|
||||
consentSchema.statics.getConsentHistory = function(userId, type) {
|
||||
const query = { userId };
|
||||
if (type) query.type = type;
|
||||
|
||||
return this.find(query).sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
export const ConsentRecord = mongoose.model('ConsentRecord', consentSchema);
|
||||
@@ -0,0 +1,266 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const dataRequestSchema = new mongoose.Schema({
|
||||
// Multi-tenant support
|
||||
tenantId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Tenant',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
requestId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
accountId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['access', 'portability', 'deletion', 'rectification', 'restriction', 'objection']
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['pending', 'processing', 'completed', 'rejected', 'expired'],
|
||||
default: 'pending'
|
||||
},
|
||||
priority: {
|
||||
type: String,
|
||||
enum: ['low', 'medium', 'high', 'urgent'],
|
||||
default: 'medium'
|
||||
},
|
||||
requestDetails: {
|
||||
scope: {
|
||||
type: String,
|
||||
enum: ['all', 'specific'],
|
||||
default: 'all'
|
||||
},
|
||||
dataCategories: [String],
|
||||
dateRange: {
|
||||
start: Date,
|
||||
end: Date
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
enum: ['json', 'csv', 'pdf', 'xml'],
|
||||
default: 'json'
|
||||
},
|
||||
reason: String,
|
||||
additionalInfo: String
|
||||
},
|
||||
verification: {
|
||||
method: {
|
||||
type: String,
|
||||
enum: ['email', 'sms', 'document', 'manual']
|
||||
},
|
||||
verified: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
verifiedAt: Date,
|
||||
verifiedBy: String,
|
||||
attempts: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
processing: {
|
||||
startedAt: Date,
|
||||
completedAt: Date,
|
||||
processedBy: String,
|
||||
dataCollected: {
|
||||
categories: [String],
|
||||
recordCount: Number,
|
||||
sizeInBytes: Number
|
||||
},
|
||||
errors: [{
|
||||
timestamp: Date,
|
||||
message: String,
|
||||
service: String
|
||||
}]
|
||||
},
|
||||
response: {
|
||||
method: {
|
||||
type: String,
|
||||
enum: ['download', 'email', 'api', 'mail']
|
||||
},
|
||||
deliveredAt: Date,
|
||||
downloadUrl: String,
|
||||
downloadExpiry: Date,
|
||||
emailSent: Boolean,
|
||||
physicalMailTracking: String
|
||||
},
|
||||
compliance: {
|
||||
regulation: {
|
||||
type: String,
|
||||
enum: ['gdpr', 'ccpa', 'lgpd', 'pipeda', 'appi', 'other']
|
||||
},
|
||||
deadline: Date,
|
||||
sla: {
|
||||
type: Number,
|
||||
default: 30 // days
|
||||
},
|
||||
articles: [String], // Relevant articles/sections
|
||||
legalBasis: String
|
||||
},
|
||||
rejection: {
|
||||
reason: String,
|
||||
legalJustification: String,
|
||||
appealDeadline: Date,
|
||||
rejectedBy: String,
|
||||
rejectedAt: Date
|
||||
},
|
||||
audit: {
|
||||
ipAddress: String,
|
||||
userAgent: String,
|
||||
channel: {
|
||||
type: String,
|
||||
enum: ['web', 'email', 'phone', 'mail', 'api']
|
||||
},
|
||||
notes: [{
|
||||
timestamp: Date,
|
||||
author: String,
|
||||
note: String
|
||||
}]
|
||||
},
|
||||
costs: {
|
||||
estimatedCost: Number,
|
||||
actualCost: Number,
|
||||
currency: {
|
||||
type: String,
|
||||
default: 'USD'
|
||||
},
|
||||
waived: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Indexes
|
||||
dataRequestSchema.index({ userId: 1, type: 1, createdAt: -1 });
|
||||
dataRequestSchema.index({ status: 1, 'compliance.deadline': 1 });
|
||||
dataRequestSchema.index({ accountId: 1, status: 1 });
|
||||
|
||||
// Multi-tenant indexes
|
||||
dataRequestSchema.index({ tenantId: 1, userId: 1, type: 1, createdAt: -1 });
|
||||
dataRequestSchema.index({ tenantId: 1, status: 1, 'compliance.deadline': 1 });
|
||||
dataRequestSchema.index({ tenantId: 1, accountId: 1, status: 1 });
|
||||
|
||||
// Methods
|
||||
dataRequestSchema.methods.isOverdue = function() {
|
||||
if (this.status === 'completed' || this.status === 'rejected') return false;
|
||||
return this.compliance.deadline && this.compliance.deadline < new Date();
|
||||
};
|
||||
|
||||
dataRequestSchema.methods.getDaysRemaining = function() {
|
||||
if (!this.compliance.deadline) return null;
|
||||
const now = new Date();
|
||||
const diffTime = this.compliance.deadline - now;
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return diffDays;
|
||||
};
|
||||
|
||||
dataRequestSchema.methods.canProcess = function() {
|
||||
return this.status === 'pending' && this.verification.verified;
|
||||
};
|
||||
|
||||
dataRequestSchema.methods.markProcessing = function(processedBy) {
|
||||
this.status = 'processing';
|
||||
this.processing.startedAt = new Date();
|
||||
this.processing.processedBy = processedBy;
|
||||
};
|
||||
|
||||
dataRequestSchema.methods.markCompleted = function(responseData) {
|
||||
this.status = 'completed';
|
||||
this.processing.completedAt = new Date();
|
||||
Object.assign(this.response, responseData);
|
||||
};
|
||||
|
||||
dataRequestSchema.methods.reject = function(reason, legalJustification, rejectedBy) {
|
||||
this.status = 'rejected';
|
||||
this.rejection = {
|
||||
reason,
|
||||
legalJustification,
|
||||
rejectedBy,
|
||||
rejectedAt: new Date(),
|
||||
appealDeadline: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days
|
||||
};
|
||||
};
|
||||
|
||||
// Statics
|
||||
dataRequestSchema.statics.findPending = function(accountId) {
|
||||
const query = { status: 'pending' };
|
||||
if (accountId) query.accountId = accountId;
|
||||
|
||||
return this.find(query).sort({ createdAt: 1 });
|
||||
};
|
||||
|
||||
dataRequestSchema.statics.findOverdue = function(accountId) {
|
||||
const query = {
|
||||
status: { $in: ['pending', 'processing'] },
|
||||
'compliance.deadline': { $lt: new Date() }
|
||||
};
|
||||
|
||||
if (accountId) query.accountId = accountId;
|
||||
|
||||
return this.find(query);
|
||||
};
|
||||
|
||||
dataRequestSchema.statics.getRequestStats = async function(accountId, startDate, endDate) {
|
||||
const match = { accountId };
|
||||
if (startDate || endDate) {
|
||||
match.createdAt = {};
|
||||
if (startDate) match.createdAt.$gte = startDate;
|
||||
if (endDate) match.createdAt.$lte = endDate;
|
||||
}
|
||||
|
||||
return await this.aggregate([
|
||||
{ $match: match },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
type: '$type',
|
||||
status: '$status'
|
||||
},
|
||||
count: { $sum: 1 },
|
||||
avgProcessingTime: {
|
||||
$avg: {
|
||||
$cond: [
|
||||
{ $and: ['$processing.startedAt', '$processing.completedAt'] },
|
||||
{ $subtract: ['$processing.completedAt', '$processing.startedAt'] },
|
||||
null
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$_id.type',
|
||||
statuses: {
|
||||
$push: {
|
||||
status: '$_id.status',
|
||||
count: '$count'
|
||||
}
|
||||
},
|
||||
totalCount: { $sum: '$count' },
|
||||
avgProcessingTime: { $avg: '$avgProcessingTime' }
|
||||
}
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
export const DataRequest = mongoose.model('DataRequest', dataRequestSchema);
|
||||
364
marketing-agent/services/compliance-guard/src/routes/audit.js
Normal file
364
marketing-agent/services/compliance-guard/src/routes/audit.js
Normal file
@@ -0,0 +1,364 @@
|
||||
import express from 'express';
|
||||
import { AuditLog } from '../models/AuditLog.js';
|
||||
import { AuditService } from '../services/auditService.js';
|
||||
import { auth } from '../utils/auth.js';
|
||||
import { metrics } from '../utils/metrics.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const router = express.Router();
|
||||
const auditService = new AuditService();
|
||||
|
||||
/**
|
||||
* Search audit logs
|
||||
*/
|
||||
router.get('/audit-logs', auth(), async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
accountId,
|
||||
userId,
|
||||
category,
|
||||
action,
|
||||
resource,
|
||||
startDate,
|
||||
endDate,
|
||||
page = 1,
|
||||
limit = 50
|
||||
} = req.query;
|
||||
|
||||
const criteria = {
|
||||
accountId: accountId || req.user.accountId,
|
||||
userId,
|
||||
category,
|
||||
action,
|
||||
resource,
|
||||
startDate: startDate ? new Date(startDate) : undefined,
|
||||
endDate: endDate ? new Date(endDate) : undefined,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
};
|
||||
|
||||
const logs = await auditService.searchLogs(criteria);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: logs,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
hasMore: logs.length === parseInt(limit)
|
||||
}
|
||||
});
|
||||
|
||||
metrics.recordApiCall('audit.search', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to search audit logs:', error);
|
||||
metrics.recordApiCall('audit.search', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to search audit logs'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get audit trail for a resource
|
||||
*/
|
||||
router.get('/audit-trail/:resourceType/:resourceId', auth(), async (req, res) => {
|
||||
try {
|
||||
const { resourceType, resourceId } = req.params;
|
||||
|
||||
const trail = await auditService.getAuditTrail(resourceId, resourceType);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: trail,
|
||||
count: trail.length
|
||||
});
|
||||
|
||||
metrics.recordApiCall('audit.trail', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get audit trail:', error);
|
||||
metrics.recordApiCall('audit.trail', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get audit trail'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Generate compliance report
|
||||
*/
|
||||
router.post('/audit-reports/compliance', auth(), async (req, res) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.body;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Start date and end date are required'
|
||||
});
|
||||
}
|
||||
|
||||
const report = await auditService.generateComplianceReport(
|
||||
req.user.accountId,
|
||||
new Date(startDate),
|
||||
new Date(endDate)
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: report
|
||||
});
|
||||
|
||||
metrics.recordApiCall('audit.compliance_report', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to generate compliance report:', error);
|
||||
metrics.recordApiCall('audit.compliance_report', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to generate compliance report'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Detect anomalies
|
||||
*/
|
||||
router.get('/audit-anomalies', auth(), async (req, res) => {
|
||||
try {
|
||||
const { window = 3600000 } = req.query; // Default 1 hour
|
||||
|
||||
const anomalies = await auditService.detectAnomalies(
|
||||
req.user.accountId,
|
||||
parseInt(window)
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: anomalies,
|
||||
count: anomalies.length,
|
||||
window: parseInt(window)
|
||||
});
|
||||
|
||||
metrics.recordApiCall('audit.anomalies', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to detect anomalies:', error);
|
||||
metrics.recordApiCall('audit.anomalies', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to detect anomalies'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get activity summary
|
||||
*/
|
||||
router.get('/audit-summary', auth(), async (req, res) => {
|
||||
try {
|
||||
const { days = 7 } = req.query;
|
||||
|
||||
const summary = await AuditLog.getActivitySummary(
|
||||
req.user.accountId,
|
||||
parseInt(days)
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: summary,
|
||||
period: {
|
||||
days: parseInt(days),
|
||||
startDate: new Date(Date.now() - parseInt(days) * 24 * 60 * 60 * 1000),
|
||||
endDate: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
metrics.recordApiCall('audit.summary', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get activity summary:', error);
|
||||
metrics.recordApiCall('audit.summary', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get activity summary'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get audit log by ID
|
||||
*/
|
||||
router.get('/audit-logs/:auditId', auth(), async (req, res) => {
|
||||
try {
|
||||
const { auditId } = req.params;
|
||||
|
||||
const log = await AuditLog.findOne({ auditId });
|
||||
if (!log) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Audit log not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check access permissions
|
||||
if (log.accountId !== req.user.accountId) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Access denied'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: log
|
||||
});
|
||||
|
||||
metrics.recordApiCall('audit.get', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get audit log:', error);
|
||||
metrics.recordApiCall('audit.get', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get audit log'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Export audit logs
|
||||
*/
|
||||
router.post('/audit-export', auth(), async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
format = 'json',
|
||||
startDate,
|
||||
endDate,
|
||||
category,
|
||||
compress = false
|
||||
} = req.body;
|
||||
|
||||
const criteria = {
|
||||
accountId: req.user.accountId,
|
||||
startDate: startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||
endDate: endDate ? new Date(endDate) : new Date(),
|
||||
category
|
||||
};
|
||||
|
||||
const logs = await auditService.searchLogs(criteria);
|
||||
|
||||
let exportData;
|
||||
let contentType;
|
||||
let filename = `audit_export_${Date.now()}`;
|
||||
|
||||
switch (format) {
|
||||
case 'csv':
|
||||
exportData = convertToCSV(logs);
|
||||
contentType = 'text/csv';
|
||||
filename += '.csv';
|
||||
break;
|
||||
case 'json':
|
||||
default:
|
||||
exportData = JSON.stringify(logs, null, 2);
|
||||
contentType = 'application/json';
|
||||
filename += '.json';
|
||||
break;
|
||||
}
|
||||
|
||||
if (compress) {
|
||||
// In production, implement compression
|
||||
filename += '.gz';
|
||||
contentType = 'application/gzip';
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.send(exportData);
|
||||
|
||||
metrics.recordApiCall('audit.export', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to export audit logs:', error);
|
||||
metrics.recordApiCall('audit.export', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to export audit logs'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create manual audit log entry
|
||||
*/
|
||||
router.post('/audit-logs', auth(), async (req, res) => {
|
||||
try {
|
||||
const eventData = {
|
||||
...req.body,
|
||||
accountId: req.user.accountId,
|
||||
actor: {
|
||||
type: 'user',
|
||||
id: req.user.id,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent')
|
||||
}
|
||||
};
|
||||
|
||||
const auditLog = await auditService.logEvent(eventData);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: auditLog
|
||||
});
|
||||
|
||||
metrics.recordApiCall('audit.create', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to create audit log:', error);
|
||||
metrics.recordApiCall('audit.create', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create audit log'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to convert to CSV
|
||||
*/
|
||||
function convertToCSV(logs) {
|
||||
if (logs.length === 0) return '';
|
||||
|
||||
const headers = [
|
||||
'Timestamp',
|
||||
'Audit ID',
|
||||
'Action',
|
||||
'Category',
|
||||
'Resource',
|
||||
'Resource ID',
|
||||
'Actor Type',
|
||||
'Actor ID',
|
||||
'Result',
|
||||
'IP Address'
|
||||
];
|
||||
|
||||
const rows = logs.map(log => [
|
||||
log.timestamp,
|
||||
log.auditId,
|
||||
log.action,
|
||||
log.category,
|
||||
log.resource || '',
|
||||
log.resourceId || '',
|
||||
log.actor.type,
|
||||
log.actor.id,
|
||||
log.result.status,
|
||||
log.actor.ip || ''
|
||||
]);
|
||||
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...rows.map(row => row.map(cell =>
|
||||
typeof cell === 'string' && cell.includes(',') ? `"${cell}"` : cell
|
||||
).join(','))
|
||||
].join('\n');
|
||||
|
||||
return csvContent;
|
||||
}
|
||||
|
||||
export default router;
|
||||
369
marketing-agent/services/compliance-guard/src/routes/consent.js
Normal file
369
marketing-agent/services/compliance-guard/src/routes/consent.js
Normal file
@@ -0,0 +1,369 @@
|
||||
import express from 'express';
|
||||
import { ConsentRecord } from '../models/ConsentRecord.js';
|
||||
import { AuditLog } from '../models/AuditLog.js';
|
||||
import { auth } from '../utils/auth.js';
|
||||
import { validateConsent } from '../utils/validation.js';
|
||||
import { publishEvent } from '../services/messaging.js';
|
||||
import { metrics } from '../utils/metrics.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Get consent records for a user
|
||||
*/
|
||||
router.get('/users/:userId/consents', auth(), async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { accountId, type, status } = req.query;
|
||||
|
||||
const query = { userId };
|
||||
if (accountId) query.accountId = accountId;
|
||||
if (type) query.type = type;
|
||||
if (status) query.status = status;
|
||||
|
||||
const consents = await ConsentRecord.find(query)
|
||||
.sort({ grantedAt: -1 })
|
||||
.limit(100);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: consents,
|
||||
count: consents.length
|
||||
});
|
||||
|
||||
metrics.recordApiCall('consent.list', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get consent records:', error);
|
||||
metrics.recordApiCall('consent.list', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve consent records'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get specific consent record
|
||||
*/
|
||||
router.get('/consents/:consentId', auth(), async (req, res) => {
|
||||
try {
|
||||
const { consentId } = req.params;
|
||||
|
||||
const consent = await ConsentRecord.findOne({ consentId });
|
||||
if (!consent) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Consent record not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: consent
|
||||
});
|
||||
|
||||
metrics.recordApiCall('consent.get', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get consent record:', error);
|
||||
metrics.recordApiCall('consent.get', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve consent record'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Grant consent
|
||||
*/
|
||||
router.post('/users/:userId/consents', auth(), validateConsent, async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const consentData = {
|
||||
...req.body,
|
||||
userId,
|
||||
accountId: req.user.accountId,
|
||||
metadata: {
|
||||
ipAddress: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
location: req.body.location // Should be determined by IP in production
|
||||
}
|
||||
};
|
||||
|
||||
// Check for existing consent
|
||||
const existing = await ConsentRecord.findOne({
|
||||
userId,
|
||||
accountId: consentData.accountId,
|
||||
type: consentData.type,
|
||||
status: 'granted'
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Consent already granted for this type'
|
||||
});
|
||||
}
|
||||
|
||||
// Create consent record
|
||||
const consent = await ConsentRecord.grantConsent(consentData);
|
||||
|
||||
// Audit log
|
||||
await AuditLog.log({
|
||||
accountId: consentData.accountId,
|
||||
userId,
|
||||
action: 'consent_granted',
|
||||
category: 'consent',
|
||||
resource: 'consent_record',
|
||||
resourceId: consent.consentId,
|
||||
details: {
|
||||
type: consent.type,
|
||||
purposes: consent.purposes
|
||||
},
|
||||
result: { status: 'success' },
|
||||
actor: {
|
||||
type: 'user',
|
||||
id: userId,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent')
|
||||
}
|
||||
});
|
||||
|
||||
// Publish event
|
||||
await publishEvent('consent.granted', {
|
||||
consentId: consent.consentId,
|
||||
userId,
|
||||
type: consent.type
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: consent
|
||||
});
|
||||
|
||||
metrics.recordApiCall('consent.grant', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to grant consent:', error);
|
||||
metrics.recordApiCall('consent.grant', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to grant consent'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update consent
|
||||
*/
|
||||
router.put('/consents/:consentId', auth(), validateConsent, async (req, res) => {
|
||||
try {
|
||||
const { consentId } = req.params;
|
||||
|
||||
const consent = await ConsentRecord.findOne({ consentId });
|
||||
if (!consent) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Consent record not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Update consent
|
||||
consent.update(req.body);
|
||||
await consent.save();
|
||||
|
||||
// Audit log
|
||||
await AuditLog.log({
|
||||
accountId: consent.accountId,
|
||||
userId: consent.userId,
|
||||
action: 'consent_updated',
|
||||
category: 'consent',
|
||||
resource: 'consent_record',
|
||||
resourceId: consent.consentId,
|
||||
details: {
|
||||
changes: req.body
|
||||
},
|
||||
result: { status: 'success' },
|
||||
actor: {
|
||||
type: 'user',
|
||||
id: req.user.id,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent')
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: consent
|
||||
});
|
||||
|
||||
metrics.recordApiCall('consent.update', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to update consent:', error);
|
||||
metrics.recordApiCall('consent.update', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update consent'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Withdraw consent
|
||||
*/
|
||||
router.delete('/consents/:consentId', auth(), async (req, res) => {
|
||||
try {
|
||||
const { consentId } = req.params;
|
||||
const { reason } = req.body;
|
||||
|
||||
const consent = await ConsentRecord.findOne({ consentId });
|
||||
if (!consent) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Consent record not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Withdraw consent
|
||||
consent.withdraw(reason || 'User requested withdrawal');
|
||||
await consent.save();
|
||||
|
||||
// Audit log
|
||||
await AuditLog.log({
|
||||
accountId: consent.accountId,
|
||||
userId: consent.userId,
|
||||
action: 'consent_withdrawn',
|
||||
category: 'consent',
|
||||
resource: 'consent_record',
|
||||
resourceId: consent.consentId,
|
||||
details: {
|
||||
reason: reason || 'User requested withdrawal'
|
||||
},
|
||||
result: { status: 'success' },
|
||||
actor: {
|
||||
type: 'user',
|
||||
id: req.user.id,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent')
|
||||
}
|
||||
});
|
||||
|
||||
// Publish event
|
||||
await publishEvent('consent.withdrawn', {
|
||||
consentId: consent.consentId,
|
||||
userId: consent.userId,
|
||||
type: consent.type
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Consent withdrawn successfully'
|
||||
});
|
||||
|
||||
metrics.recordApiCall('consent.withdraw', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to withdraw consent:', error);
|
||||
metrics.recordApiCall('consent.withdraw', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to withdraw consent'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get consent history
|
||||
*/
|
||||
router.get('/users/:userId/consent-history', auth(), async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { startDate, endDate, type } = req.query;
|
||||
|
||||
const query = { userId };
|
||||
if (type) query.type = type;
|
||||
if (startDate || endDate) {
|
||||
query.grantedAt = {};
|
||||
if (startDate) query.grantedAt.$gte = new Date(startDate);
|
||||
if (endDate) query.grantedAt.$lte = new Date(endDate);
|
||||
}
|
||||
|
||||
const history = await ConsentRecord.find(query)
|
||||
.sort({ grantedAt: -1 })
|
||||
.limit(500);
|
||||
|
||||
const grouped = history.reduce((acc, consent) => {
|
||||
const key = consent.type;
|
||||
if (!acc[key]) acc[key] = [];
|
||||
acc[key].push({
|
||||
consentId: consent.consentId,
|
||||
status: consent.status,
|
||||
grantedAt: consent.grantedAt,
|
||||
withdrawnAt: consent.withdrawnAt,
|
||||
expiryDate: consent.expiryDate,
|
||||
version: consent.version
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: grouped,
|
||||
totalRecords: history.length
|
||||
});
|
||||
|
||||
metrics.recordApiCall('consent.history', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get consent history:', error);
|
||||
metrics.recordApiCall('consent.history', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve consent history'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify consent for specific purpose
|
||||
*/
|
||||
router.post('/verify-consent', auth(), async (req, res) => {
|
||||
try {
|
||||
const { userId, type, purpose } = req.body;
|
||||
|
||||
const consent = await ConsentRecord.findOne({
|
||||
userId,
|
||||
type,
|
||||
status: 'granted'
|
||||
});
|
||||
|
||||
if (!consent) {
|
||||
return res.json({
|
||||
success: true,
|
||||
hasConsent: false,
|
||||
reason: 'No consent record found'
|
||||
});
|
||||
}
|
||||
|
||||
const isValid = consent.isValid();
|
||||
const hasPurpose = !purpose || consent.purposes.includes(purpose);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
hasConsent: isValid && hasPurpose,
|
||||
consentId: consent.consentId,
|
||||
grantedAt: consent.grantedAt,
|
||||
expiryDate: consent.expiryDate,
|
||||
reason: !isValid ? 'Consent expired or withdrawn' : !hasPurpose ? 'Purpose not consented' : null
|
||||
});
|
||||
|
||||
metrics.recordApiCall('consent.verify', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to verify consent:', error);
|
||||
metrics.recordApiCall('consent.verify', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to verify consent'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,312 @@
|
||||
import express from 'express';
|
||||
import { DataRequest } from '../models/DataRequest.js';
|
||||
import { DataRequestProcessor } from '../services/dataRequestProcessor.js';
|
||||
import { auth } from '../utils/auth.js';
|
||||
import { validateDataRequest } from '../utils/validation.js';
|
||||
import { metrics } from '../utils/metrics.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Initialize data request processor
|
||||
let dataRequestProcessor;
|
||||
|
||||
export const initializeDataRequestRoutes = (redisClient) => {
|
||||
dataRequestProcessor = new DataRequestProcessor(redisClient);
|
||||
return router;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create data request
|
||||
*/
|
||||
router.post('/data-requests', auth(), validateDataRequest, async (req, res) => {
|
||||
try {
|
||||
const requestData = {
|
||||
...req.body,
|
||||
accountId: req.user.accountId,
|
||||
audit: {
|
||||
ipAddress: req.ip,
|
||||
userAgent: req.get('user-agent')
|
||||
}
|
||||
};
|
||||
|
||||
const dataRequest = await dataRequestProcessor.createDataRequest(requestData);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: dataRequest,
|
||||
message: 'Data request created successfully'
|
||||
});
|
||||
|
||||
metrics.recordApiCall('data_request.create', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to create data request:', error);
|
||||
metrics.recordApiCall('data_request.create', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create data request'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get data request by ID
|
||||
*/
|
||||
router.get('/data-requests/:requestId', auth(), async (req, res) => {
|
||||
try {
|
||||
const { requestId } = req.params;
|
||||
|
||||
const dataRequest = await DataRequest.findOne({ requestId });
|
||||
if (!dataRequest) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Data request not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check access permissions
|
||||
if (dataRequest.accountId !== req.user.accountId &&
|
||||
dataRequest.userId !== req.user.id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Access denied'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: dataRequest
|
||||
});
|
||||
|
||||
metrics.recordApiCall('data_request.get', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get data request:', error);
|
||||
metrics.recordApiCall('data_request.get', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve data request'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* List data requests
|
||||
*/
|
||||
router.get('/data-requests', auth(), async (req, res) => {
|
||||
try {
|
||||
const { status, type, userId, startDate, endDate, page = 1, limit = 20 } = req.query;
|
||||
|
||||
const query = { accountId: req.user.accountId };
|
||||
if (status) query.status = status;
|
||||
if (type) query.type = type;
|
||||
if (userId) query.userId = userId;
|
||||
if (startDate || endDate) {
|
||||
query.createdAt = {};
|
||||
if (startDate) query.createdAt.$gte = new Date(startDate);
|
||||
if (endDate) query.createdAt.$lte = new Date(endDate);
|
||||
}
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [requests, total] = await Promise.all([
|
||||
DataRequest.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
DataRequest.countDocuments(query)
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: requests,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
});
|
||||
|
||||
metrics.recordApiCall('data_request.list', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to list data requests:', error);
|
||||
metrics.recordApiCall('data_request.list', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to list data requests'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify data request
|
||||
*/
|
||||
router.post('/data-requests/:requestId/verify', auth(), async (req, res) => {
|
||||
try {
|
||||
const { requestId } = req.params;
|
||||
const { method } = req.body;
|
||||
|
||||
const verificationData = {
|
||||
verifiedBy: req.user.id,
|
||||
method: method || 'manual'
|
||||
};
|
||||
|
||||
const dataRequest = await dataRequestProcessor.verifyRequest(requestId, verificationData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: dataRequest,
|
||||
message: 'Data request verified successfully'
|
||||
});
|
||||
|
||||
metrics.recordApiCall('data_request.verify', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to verify data request:', error);
|
||||
metrics.recordApiCall('data_request.verify', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || 'Failed to verify data request'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Process data request
|
||||
*/
|
||||
router.post('/data-requests/:requestId/process', auth(), async (req, res) => {
|
||||
try {
|
||||
const { requestId } = req.params;
|
||||
|
||||
// Start processing asynchronously
|
||||
dataRequestProcessor.processRequest(requestId)
|
||||
.catch(error => {
|
||||
logger.error('Data request processing failed:', error);
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Data request processing started'
|
||||
});
|
||||
|
||||
metrics.recordApiCall('data_request.process', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to start data request processing:', error);
|
||||
metrics.recordApiCall('data_request.process', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to start data request processing'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Download data
|
||||
*/
|
||||
router.get('/download/:token', async (req, res) => {
|
||||
try {
|
||||
const { token } = req.params;
|
||||
|
||||
// Retrieve data using token
|
||||
const encryptedData = await dataRequestProcessor.redis.get(`download:${token}`);
|
||||
if (!encryptedData) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Download link expired or invalid'
|
||||
});
|
||||
}
|
||||
|
||||
// Decrypt data
|
||||
const { data, format, requestId } = dataRequestProcessor.dataProtection.decrypt(encryptedData);
|
||||
|
||||
// Set appropriate headers
|
||||
const filename = `data_export_${requestId}.${format}`;
|
||||
res.setHeader('Content-Type', getContentType(format));
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
|
||||
// Send data
|
||||
res.send(data);
|
||||
|
||||
// Delete token after download
|
||||
await dataRequestProcessor.redis.del(`download:${token}`);
|
||||
|
||||
metrics.recordApiCall('data_request.download', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to download data:', error);
|
||||
metrics.recordApiCall('data_request.download', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to download data'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get data request statistics
|
||||
*/
|
||||
router.get('/data-requests/stats', auth(), async (req, res) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
const stats = await DataRequest.getRequestStats(
|
||||
req.user.accountId,
|
||||
startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||
endDate ? new Date(endDate) : new Date()
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats
|
||||
});
|
||||
|
||||
metrics.recordApiCall('data_request.stats', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get data request statistics:', error);
|
||||
metrics.recordApiCall('data_request.stats', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get statistics'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get overdue requests
|
||||
*/
|
||||
router.get('/data-requests/overdue', auth(), async (req, res) => {
|
||||
try {
|
||||
const overdueRequests = await DataRequest.findOverdue(req.user.accountId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: overdueRequests,
|
||||
count: overdueRequests.length
|
||||
});
|
||||
|
||||
metrics.recordApiCall('data_request.overdue', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get overdue requests:', error);
|
||||
metrics.recordApiCall('data_request.overdue', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get overdue requests'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to get content type
|
||||
*/
|
||||
function getContentType(format) {
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return 'application/json';
|
||||
case 'csv':
|
||||
return 'text/csv';
|
||||
case 'xml':
|
||||
return 'application/xml';
|
||||
default:
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,426 @@
|
||||
import express from 'express';
|
||||
import { ComplianceViolation } from '../models/ComplianceViolation.js';
|
||||
import { AuditLog } from '../models/AuditLog.js';
|
||||
import { auth } from '../utils/auth.js';
|
||||
import { validateViolation } from '../utils/validation.js';
|
||||
import { publishEvent } from '../services/messaging.js';
|
||||
import { metrics } from '../utils/metrics.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Create violation
|
||||
*/
|
||||
router.post('/violations', auth(), validateViolation, async (req, res) => {
|
||||
try {
|
||||
const violationData = {
|
||||
...req.body,
|
||||
accountId: req.user.accountId,
|
||||
violationId: `vio_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
timeline: {
|
||||
detectedAt: new Date()
|
||||
}
|
||||
};
|
||||
|
||||
const violation = new ComplianceViolation(violationData);
|
||||
violation.calculateRiskScore();
|
||||
await violation.save();
|
||||
|
||||
// Audit log
|
||||
await AuditLog.log({
|
||||
accountId: req.user.accountId,
|
||||
userId: req.user.id,
|
||||
action: 'violation_created',
|
||||
category: 'compliance',
|
||||
resource: 'violation',
|
||||
resourceId: violation.violationId,
|
||||
details: {
|
||||
type: violation.type,
|
||||
severity: violation.severity,
|
||||
riskScore: violation.risk.score
|
||||
},
|
||||
result: { status: 'success' },
|
||||
actor: {
|
||||
type: 'user',
|
||||
id: req.user.id,
|
||||
ip: req.ip
|
||||
}
|
||||
});
|
||||
|
||||
// Publish event
|
||||
await publishEvent('violation.created', {
|
||||
violationId: violation.violationId,
|
||||
type: violation.type,
|
||||
severity: violation.severity
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: violation
|
||||
});
|
||||
|
||||
metrics.recordApiCall('violation.create', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to create violation:', error);
|
||||
metrics.recordApiCall('violation.create', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create violation'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get violation by ID
|
||||
*/
|
||||
router.get('/violations/:violationId', auth(), async (req, res) => {
|
||||
try {
|
||||
const { violationId } = req.params;
|
||||
|
||||
const violation = await ComplianceViolation.findOne({ violationId });
|
||||
if (!violation) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Violation not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check access permissions
|
||||
if (violation.accountId !== req.user.accountId) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Access denied'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: violation
|
||||
});
|
||||
|
||||
metrics.recordApiCall('violation.get', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get violation:', error);
|
||||
metrics.recordApiCall('violation.get', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to retrieve violation'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* List violations
|
||||
*/
|
||||
router.get('/violations', auth(), async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
status,
|
||||
severity,
|
||||
type,
|
||||
regulation,
|
||||
startDate,
|
||||
endDate,
|
||||
page = 1,
|
||||
limit = 20
|
||||
} = req.query;
|
||||
|
||||
const query = { accountId: req.user.accountId };
|
||||
if (status) query.status = status;
|
||||
if (severity) query.severity = severity;
|
||||
if (type) query.type = type;
|
||||
if (regulation) query['details.regulation'] = regulation;
|
||||
if (startDate || endDate) {
|
||||
query['timeline.detectedAt'] = {};
|
||||
if (startDate) query['timeline.detectedAt'].$gte = new Date(startDate);
|
||||
if (endDate) query['timeline.detectedAt'].$lte = new Date(endDate);
|
||||
}
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [violations, total] = await Promise.all([
|
||||
ComplianceViolation.find(query)
|
||||
.sort({ 'timeline.detectedAt': -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
ComplianceViolation.countDocuments(query)
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: violations,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
});
|
||||
|
||||
metrics.recordApiCall('violation.list', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to list violations:', error);
|
||||
metrics.recordApiCall('violation.list', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to list violations'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Update violation status
|
||||
*/
|
||||
router.put('/violations/:violationId/status', auth(), async (req, res) => {
|
||||
try {
|
||||
const { violationId } = req.params;
|
||||
const { status, reason } = req.body;
|
||||
|
||||
const violation = await ComplianceViolation.findOne({ violationId });
|
||||
if (!violation) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Violation not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check access permissions
|
||||
if (violation.accountId !== req.user.accountId) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Access denied'
|
||||
});
|
||||
}
|
||||
|
||||
const oldStatus = violation.status;
|
||||
violation.status = status;
|
||||
|
||||
// Update timeline based on status
|
||||
if (status === 'resolved') {
|
||||
violation.timeline.resolvedAt = new Date();
|
||||
violation.remediation.status = 'completed';
|
||||
} else if (status === 'mitigated') {
|
||||
violation.timeline.mitigatedAt = new Date();
|
||||
}
|
||||
|
||||
await violation.save();
|
||||
|
||||
// Audit log
|
||||
await AuditLog.log({
|
||||
accountId: req.user.accountId,
|
||||
userId: req.user.id,
|
||||
action: 'violation_status_updated',
|
||||
category: 'compliance',
|
||||
resource: 'violation',
|
||||
resourceId: violation.violationId,
|
||||
details: {
|
||||
oldStatus,
|
||||
newStatus: status,
|
||||
reason
|
||||
},
|
||||
result: { status: 'success' },
|
||||
actor: {
|
||||
type: 'user',
|
||||
id: req.user.id,
|
||||
ip: req.ip
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: violation
|
||||
});
|
||||
|
||||
metrics.recordApiCall('violation.update_status', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to update violation status:', error);
|
||||
metrics.recordApiCall('violation.update_status', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to update violation status'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Add remediation action
|
||||
*/
|
||||
router.post('/violations/:violationId/remediation', auth(), async (req, res) => {
|
||||
try {
|
||||
const { violationId } = req.params;
|
||||
const { action, description, targetDate } = req.body;
|
||||
|
||||
const violation = await ComplianceViolation.findOne({ violationId });
|
||||
if (!violation) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Violation not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Add remediation action
|
||||
violation.remediation.actions.push({
|
||||
action,
|
||||
description,
|
||||
targetDate: targetDate ? new Date(targetDate) : undefined,
|
||||
createdAt: new Date(),
|
||||
createdBy: req.user.id,
|
||||
status: 'pending'
|
||||
});
|
||||
|
||||
violation.remediation.status = 'in_progress';
|
||||
await violation.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: violation
|
||||
});
|
||||
|
||||
metrics.recordApiCall('violation.add_remediation', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to add remediation action:', error);
|
||||
metrics.recordApiCall('violation.add_remediation', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to add remediation action'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Add notification record
|
||||
*/
|
||||
router.post('/violations/:violationId/notifications', auth(), async (req, res) => {
|
||||
try {
|
||||
const { violationId } = req.params;
|
||||
const { type, recipients, method, content } = req.body;
|
||||
|
||||
const violation = await ComplianceViolation.findOne({ violationId });
|
||||
if (!violation) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Violation not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Add notification based on type
|
||||
const notification = {
|
||||
notifiedAt: new Date(),
|
||||
method,
|
||||
reference: `NOT-${violationId}-${Date.now()}`
|
||||
};
|
||||
|
||||
if (type === 'users') {
|
||||
violation.notifications.users.push({
|
||||
...notification,
|
||||
recipients,
|
||||
content
|
||||
});
|
||||
} else if (type === 'authorities') {
|
||||
violation.notifications.authorities.push({
|
||||
...notification,
|
||||
name: recipients.name || 'Data Protection Authority',
|
||||
jurisdiction: recipients.jurisdiction
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'authorities') {
|
||||
violation.timeline.notifiedAt = new Date();
|
||||
}
|
||||
|
||||
await violation.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: violation
|
||||
});
|
||||
|
||||
metrics.recordApiCall('violation.add_notification', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to add notification record:', error);
|
||||
metrics.recordApiCall('violation.add_notification', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to add notification record'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get violation statistics
|
||||
*/
|
||||
router.get('/violations/stats', auth(), async (req, res) => {
|
||||
try {
|
||||
const { days = 30 } = req.query;
|
||||
|
||||
const stats = await ComplianceViolation.getViolationStats(
|
||||
req.user.accountId,
|
||||
parseInt(days)
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats
|
||||
});
|
||||
|
||||
metrics.recordApiCall('violation.stats', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get violation statistics:', error);
|
||||
metrics.recordApiCall('violation.stats', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get statistics'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get active violations
|
||||
*/
|
||||
router.get('/violations/active', auth(), async (req, res) => {
|
||||
try {
|
||||
const activeViolations = await ComplianceViolation.findActive(req.user.accountId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: activeViolations,
|
||||
count: activeViolations.length
|
||||
});
|
||||
|
||||
metrics.recordApiCall('violation.active', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get active violations:', error);
|
||||
metrics.recordApiCall('violation.active', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get active violations'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get violations requiring notification
|
||||
*/
|
||||
router.get('/violations/requiring-notification', auth(), async (req, res) => {
|
||||
try {
|
||||
const violations = await ComplianceViolation.findRequiringNotification(req.user.accountId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: violations,
|
||||
count: violations.length
|
||||
});
|
||||
|
||||
metrics.recordApiCall('violation.requiring_notification', 'success');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get violations requiring notification:', error);
|
||||
metrics.recordApiCall('violation.requiring_notification', 'error');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get violations requiring notification'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,364 @@
|
||||
import { AuditLog } from '../models/AuditLog.js';
|
||||
import { DataProtectionService } from './dataProtection.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { config } from '../config/index.js';
|
||||
|
||||
export class AuditService {
|
||||
constructor() {
|
||||
this.dataProtection = new DataProtectionService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an audit event
|
||||
*/
|
||||
async logEvent(eventData) {
|
||||
try {
|
||||
// Add security metadata
|
||||
const secureEventData = {
|
||||
...eventData,
|
||||
security: {
|
||||
encrypted: true,
|
||||
signed: true,
|
||||
hash: this.dataProtection.hash(eventData),
|
||||
signature: this.generateSignature(eventData)
|
||||
}
|
||||
};
|
||||
|
||||
// Create audit log
|
||||
const auditLog = await AuditLog.log(secureEventData);
|
||||
|
||||
// Log to audit file (separate from application logs)
|
||||
this.logToFile(auditLog);
|
||||
|
||||
return auditLog;
|
||||
} catch (error) {
|
||||
logger.error('Failed to create audit log:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search audit logs
|
||||
*/
|
||||
async searchLogs(criteria) {
|
||||
try {
|
||||
const logs = await AuditLog.search(criteria);
|
||||
|
||||
// Decrypt sensitive data if needed
|
||||
const decryptedLogs = logs.map(log => {
|
||||
if (log.security?.encrypted && log.details) {
|
||||
try {
|
||||
// Decrypt only for authorized access
|
||||
log.details = this.decryptAuditData(log.details);
|
||||
} catch (error) {
|
||||
logger.warn('Failed to decrypt audit log details:', error);
|
||||
}
|
||||
}
|
||||
return log;
|
||||
});
|
||||
|
||||
return decryptedLogs;
|
||||
} catch (error) {
|
||||
logger.error('Audit log search failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit trail for a specific resource
|
||||
*/
|
||||
async getAuditTrail(resourceId, resourceType) {
|
||||
const logs = await AuditLog.find({
|
||||
$or: [
|
||||
{ resourceId },
|
||||
{ resource: resourceType, 'details.resourceId': resourceId }
|
||||
]
|
||||
}).sort({ timestamp: -1 });
|
||||
|
||||
return this.formatAuditTrail(logs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate compliance report from audit logs
|
||||
*/
|
||||
async generateComplianceReport(accountId, startDate, endDate) {
|
||||
const criteria = {
|
||||
accountId,
|
||||
startDate,
|
||||
endDate
|
||||
};
|
||||
|
||||
// Get various audit statistics
|
||||
const [
|
||||
activitySummary,
|
||||
consentActions,
|
||||
dataRequests,
|
||||
securityEvents,
|
||||
configChanges
|
||||
] = await Promise.all([
|
||||
AuditLog.getActivitySummary(accountId, Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24))),
|
||||
this.getConsentAuditStats(criteria),
|
||||
this.getDataRequestAuditStats(criteria),
|
||||
this.getSecurityAuditStats(criteria),
|
||||
this.getConfigurationAuditStats(criteria)
|
||||
]);
|
||||
|
||||
return {
|
||||
period: {
|
||||
start: startDate,
|
||||
end: endDate
|
||||
},
|
||||
summary: {
|
||||
totalEvents: this.calculateTotalEvents(activitySummary),
|
||||
successRate: this.calculateSuccessRate(activitySummary),
|
||||
topActions: this.getTopActions(activitySummary)
|
||||
},
|
||||
consent: consentActions,
|
||||
dataRequests,
|
||||
security: securityEvents,
|
||||
configuration: configChanges,
|
||||
compliance: {
|
||||
gdprCompliant: this.checkGDPRCompliance(consentActions, dataRequests),
|
||||
ccpaCompliant: this.checkCCPACompliance(consentActions, dataRequests)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor suspicious activities
|
||||
*/
|
||||
async detectAnomalies(accountId, window = 3600000) { // 1 hour window
|
||||
const recentLogs = await AuditLog.find({
|
||||
accountId,
|
||||
timestamp: { $gte: new Date(Date.now() - window) }
|
||||
});
|
||||
|
||||
const anomalies = [];
|
||||
|
||||
// Check for unusual patterns
|
||||
const activityByUser = {};
|
||||
const activityByIP = {};
|
||||
|
||||
for (const log of recentLogs) {
|
||||
// Track by user
|
||||
const userId = log.userId || log.actor?.id;
|
||||
if (userId) {
|
||||
activityByUser[userId] = (activityByUser[userId] || 0) + 1;
|
||||
}
|
||||
|
||||
// Track by IP
|
||||
const ip = log.actor?.ip;
|
||||
if (ip) {
|
||||
activityByIP[ip] = (activityByIP[ip] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect high-frequency actions
|
||||
for (const [userId, count] of Object.entries(activityByUser)) {
|
||||
if (count > 100) { // Threshold
|
||||
anomalies.push({
|
||||
type: 'high_frequency_user',
|
||||
userId,
|
||||
count,
|
||||
severity: count > 500 ? 'high' : 'medium'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Detect suspicious IPs
|
||||
for (const [ip, count] of Object.entries(activityByIP)) {
|
||||
if (count > 50) { // Threshold
|
||||
anomalies.push({
|
||||
type: 'high_frequency_ip',
|
||||
ip,
|
||||
count,
|
||||
severity: count > 200 ? 'high' : 'medium'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for failed authentication attempts
|
||||
const failedAuth = recentLogs.filter(log =>
|
||||
log.action.includes('login') && log.result.status === 'failure'
|
||||
);
|
||||
|
||||
if (failedAuth.length > 10) {
|
||||
anomalies.push({
|
||||
type: 'multiple_failed_logins',
|
||||
count: failedAuth.length,
|
||||
severity: 'high'
|
||||
});
|
||||
}
|
||||
|
||||
return anomalies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods
|
||||
*/
|
||||
generateSignature(data) {
|
||||
// Generate digital signature for audit log integrity
|
||||
return this.dataProtection.hash(JSON.stringify(data) + config.jwt.secret);
|
||||
}
|
||||
|
||||
decryptAuditData(encryptedData) {
|
||||
// Decrypt audit data (with proper authorization checks in production)
|
||||
return this.dataProtection.decrypt(encryptedData);
|
||||
}
|
||||
|
||||
formatAuditTrail(logs) {
|
||||
return logs.map(log => ({
|
||||
timestamp: log.timestamp,
|
||||
action: log.action,
|
||||
actor: `${log.actor.type}:${log.actor.id}`,
|
||||
result: log.result.status,
|
||||
changes: log.details?.changes || [],
|
||||
metadata: {
|
||||
ip: log.actor.ip,
|
||||
userAgent: log.actor.userAgent
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async getConsentAuditStats(criteria) {
|
||||
const consentLogs = await AuditLog.search({
|
||||
...criteria,
|
||||
category: 'consent'
|
||||
});
|
||||
|
||||
return {
|
||||
total: consentLogs.length,
|
||||
granted: consentLogs.filter(l => l.action === 'consent_granted').length,
|
||||
withdrawn: consentLogs.filter(l => l.action === 'consent_withdrawn').length,
|
||||
updated: consentLogs.filter(l => l.action === 'consent_updated').length
|
||||
};
|
||||
}
|
||||
|
||||
async getDataRequestAuditStats(criteria) {
|
||||
const requestLogs = await AuditLog.search({
|
||||
...criteria,
|
||||
category: 'data_access'
|
||||
});
|
||||
|
||||
return {
|
||||
total: requestLogs.length,
|
||||
access: requestLogs.filter(l => l.action.includes('access')).length,
|
||||
deletion: requestLogs.filter(l => l.action.includes('deletion')).length,
|
||||
portability: requestLogs.filter(l => l.action.includes('portability')).length,
|
||||
completed: requestLogs.filter(l => l.result.status === 'success').length
|
||||
};
|
||||
}
|
||||
|
||||
async getSecurityAuditStats(criteria) {
|
||||
const securityLogs = await AuditLog.search({
|
||||
...criteria,
|
||||
category: 'security'
|
||||
});
|
||||
|
||||
return {
|
||||
total: securityLogs.length,
|
||||
breaches: securityLogs.filter(l => l.action.includes('breach')).length,
|
||||
violations: securityLogs.filter(l => l.action.includes('violation')).length,
|
||||
authentications: securityLogs.filter(l => l.action.includes('auth')).length
|
||||
};
|
||||
}
|
||||
|
||||
async getConfigurationAuditStats(criteria) {
|
||||
const configLogs = await AuditLog.search({
|
||||
...criteria,
|
||||
category: 'configuration'
|
||||
});
|
||||
|
||||
return {
|
||||
total: configLogs.length,
|
||||
changes: configLogs.filter(l => l.result.status === 'success').length,
|
||||
byResource: this.groupByResource(configLogs)
|
||||
};
|
||||
}
|
||||
|
||||
calculateTotalEvents(summary) {
|
||||
return summary.reduce((total, day) => total + day.total, 0);
|
||||
}
|
||||
|
||||
calculateSuccessRate(summary) {
|
||||
const totals = summary.reduce((acc, day) => ({
|
||||
total: acc.total + day.total,
|
||||
success: acc.success + day.success
|
||||
}), { total: 0, success: 0 });
|
||||
|
||||
return totals.total > 0 ? (totals.success / totals.total * 100).toFixed(2) : 0;
|
||||
}
|
||||
|
||||
getTopActions(summary) {
|
||||
const actionCounts = {};
|
||||
|
||||
summary.forEach(day => {
|
||||
if (!actionCounts[day._id.category]) {
|
||||
actionCounts[day._id.category] = 0;
|
||||
}
|
||||
actionCounts[day._id.category] += day.total;
|
||||
});
|
||||
|
||||
return Object.entries(actionCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 5)
|
||||
.map(([action, count]) => ({ action, count }));
|
||||
}
|
||||
|
||||
groupByResource(logs) {
|
||||
const grouped = {};
|
||||
|
||||
logs.forEach(log => {
|
||||
if (!grouped[log.resource]) {
|
||||
grouped[log.resource] = 0;
|
||||
}
|
||||
grouped[log.resource]++;
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
checkGDPRCompliance(consentStats, requestStats) {
|
||||
// Basic GDPR compliance checks
|
||||
const consentRate = consentStats.total > 0
|
||||
? (consentStats.granted / consentStats.total * 100)
|
||||
: 0;
|
||||
|
||||
const requestCompletionRate = requestStats.total > 0
|
||||
? (requestStats.completed / requestStats.total * 100)
|
||||
: 100;
|
||||
|
||||
return {
|
||||
consentManagement: consentRate > 0,
|
||||
dataRequestHandling: requestCompletionRate > 95,
|
||||
rightToAccess: requestStats.access > 0 || requestStats.total === 0,
|
||||
rightToDeletion: requestStats.deletion > 0 || requestStats.total === 0,
|
||||
overallCompliant: consentRate > 0 && requestCompletionRate > 95
|
||||
};
|
||||
}
|
||||
|
||||
checkCCPACompliance(consentStats, requestStats) {
|
||||
// Basic CCPA compliance checks
|
||||
return {
|
||||
optOutAvailable: consentStats.withdrawn > 0 || consentStats.total === 0,
|
||||
dataAccessProvided: requestStats.access > 0 || requestStats.total === 0,
|
||||
deletionRequestsHandled: requestStats.deletion > 0 || requestStats.total === 0,
|
||||
overallCompliant: true
|
||||
};
|
||||
}
|
||||
|
||||
logToFile(auditLog) {
|
||||
// In production, this would write to a secure, tamper-proof audit file
|
||||
const auditLine = JSON.stringify({
|
||||
timestamp: auditLog.timestamp,
|
||||
auditId: auditLog.auditId,
|
||||
action: auditLog.action,
|
||||
actor: auditLog.actor,
|
||||
result: auditLog.result.status,
|
||||
hash: auditLog.security.hash
|
||||
});
|
||||
|
||||
// Append to audit file
|
||||
logger.info(`AUDIT: ${auditLine}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
import cron from 'node-cron';
|
||||
import geoip from 'geoip-lite';
|
||||
import { ConsentRecord } from '../models/ConsentRecord.js';
|
||||
import { DataRequest } from '../models/DataRequest.js';
|
||||
import { ComplianceViolation } from '../models/ComplianceViolation.js';
|
||||
import { AuditLog } from '../models/AuditLog.js';
|
||||
import { publishEvent } from './messaging.js';
|
||||
import { config } from '../config/index.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
export class ComplianceMonitor {
|
||||
constructor(redisClient) {
|
||||
this.redis = redisClient;
|
||||
this.scheduledTasks = [];
|
||||
this.violationThreshold = config.monitoring.violationThreshold;
|
||||
this.checkInterval = config.monitoring.checkInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start compliance monitoring
|
||||
*/
|
||||
async start() {
|
||||
logger.info('Starting compliance monitor');
|
||||
|
||||
// Schedule regular compliance checks
|
||||
this.scheduleTask('*/5 * * * *', this.checkConsentExpiry.bind(this));
|
||||
this.scheduleTask('*/10 * * * *', this.checkDataRequestDeadlines.bind(this));
|
||||
this.scheduleTask('*/15 * * * *', this.checkRetentionPolicies.bind(this));
|
||||
this.scheduleTask('0 * * * *', this.checkViolations.bind(this));
|
||||
this.scheduleTask('0 0 * * *', this.generateComplianceReport.bind(this));
|
||||
|
||||
// Initial checks
|
||||
await this.performInitialChecks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop compliance monitoring
|
||||
*/
|
||||
async stop() {
|
||||
logger.info('Stopping compliance monitor');
|
||||
|
||||
for (const task of this.scheduledTasks) {
|
||||
task.stop();
|
||||
}
|
||||
|
||||
this.scheduledTasks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a monitoring task
|
||||
*/
|
||||
scheduleTask(cronPattern, taskFunction) {
|
||||
const task = cron.schedule(cronPattern, async () => {
|
||||
try {
|
||||
await taskFunction();
|
||||
} catch (error) {
|
||||
logger.error('Scheduled task error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.scheduledTasks.push(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform initial compliance checks
|
||||
*/
|
||||
async performInitialChecks() {
|
||||
try {
|
||||
await this.checkConsentExpiry();
|
||||
await this.checkDataRequestDeadlines();
|
||||
await this.checkViolations();
|
||||
} catch (error) {
|
||||
logger.error('Initial compliance check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for expired consents
|
||||
*/
|
||||
async checkConsentExpiry() {
|
||||
try {
|
||||
// Find consents expiring soon
|
||||
const expiryThreshold = new Date();
|
||||
expiryThreshold.setDate(expiryThreshold.getDate() + 30);
|
||||
|
||||
const expiringConsents = await ConsentRecord.find({
|
||||
status: 'granted',
|
||||
renewable: true,
|
||||
expiryDate: {
|
||||
$gte: new Date(),
|
||||
$lte: expiryThreshold
|
||||
}
|
||||
});
|
||||
|
||||
for (const consent of expiringConsents) {
|
||||
if (consent.needsRenewal()) {
|
||||
await this.handleConsentExpiry(consent);
|
||||
}
|
||||
}
|
||||
|
||||
// Find already expired consents
|
||||
const expiredConsents = await ConsentRecord.find({
|
||||
status: 'granted',
|
||||
expiryDate: { $lt: new Date() }
|
||||
});
|
||||
|
||||
for (const consent of expiredConsents) {
|
||||
await this.handleExpiredConsent(consent);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Consent expiry check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle consent expiry
|
||||
*/
|
||||
async handleConsentExpiry(consent) {
|
||||
// Send renewal reminder
|
||||
await publishEvent('consent.expiring', {
|
||||
consentId: consent.consentId,
|
||||
userId: consent.userId,
|
||||
type: consent.type,
|
||||
expiryDate: consent.expiryDate,
|
||||
daysRemaining: Math.ceil((consent.expiryDate - new Date()) / (1000 * 60 * 60 * 24))
|
||||
});
|
||||
|
||||
// Log event
|
||||
await AuditLog.log({
|
||||
accountId: consent.accountId,
|
||||
userId: consent.userId,
|
||||
action: 'consent_expiry_notification',
|
||||
category: 'consent',
|
||||
resource: 'consent_record',
|
||||
resourceId: consent.consentId,
|
||||
details: {
|
||||
type: consent.type,
|
||||
expiryDate: consent.expiryDate
|
||||
},
|
||||
result: { status: 'success' },
|
||||
actor: { type: 'system', id: 'compliance-monitor' }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle expired consent
|
||||
*/
|
||||
async handleExpiredConsent(consent) {
|
||||
// Automatically withdraw expired consent
|
||||
consent.withdraw('Consent expired');
|
||||
await consent.save();
|
||||
|
||||
// Create violation if data is still being processed
|
||||
await this.createViolation({
|
||||
type: 'consent_expired',
|
||||
severity: 'medium',
|
||||
details: {
|
||||
description: `Consent expired for user ${consent.userId}`,
|
||||
affectedUsers: 1,
|
||||
affectedData: [consent.type],
|
||||
regulation: this.getRegulationByRegion(consent.metadata?.location?.country)
|
||||
}
|
||||
}, consent.accountId);
|
||||
|
||||
await publishEvent('consent.expired', {
|
||||
consentId: consent.consentId,
|
||||
userId: consent.userId,
|
||||
type: consent.type
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check data request deadlines
|
||||
*/
|
||||
async checkDataRequestDeadlines() {
|
||||
try {
|
||||
const overdueRequests = await DataRequest.findOverdue();
|
||||
|
||||
for (const request of overdueRequests) {
|
||||
await this.handleOverdueDataRequest(request);
|
||||
}
|
||||
|
||||
// Check requests approaching deadline
|
||||
const upcomingDeadlines = await DataRequest.find({
|
||||
status: { $in: ['pending', 'processing'] },
|
||||
'compliance.deadline': {
|
||||
$gte: new Date(),
|
||||
$lte: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000) // 3 days
|
||||
}
|
||||
});
|
||||
|
||||
for (const request of upcomingDeadlines) {
|
||||
await publishEvent('data_request.deadline_approaching', {
|
||||
requestId: request.requestId,
|
||||
type: request.type,
|
||||
daysRemaining: request.getDaysRemaining()
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Data request deadline check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle overdue data request
|
||||
*/
|
||||
async handleOverdueDataRequest(request) {
|
||||
await this.createViolation({
|
||||
type: 'data_access_denied',
|
||||
severity: 'high',
|
||||
details: {
|
||||
description: `Data request ${request.requestId} is overdue`,
|
||||
affectedUsers: 1,
|
||||
regulation: request.compliance.regulation,
|
||||
articles: request.compliance.articles,
|
||||
detectionMethod: 'automated'
|
||||
}
|
||||
}, request.accountId);
|
||||
|
||||
await publishEvent('data_request.overdue', {
|
||||
requestId: request.requestId,
|
||||
type: request.type,
|
||||
userId: request.userId,
|
||||
daysOverdue: Math.abs(request.getDaysRemaining())
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check data retention policies
|
||||
*/
|
||||
async checkRetentionPolicies() {
|
||||
try {
|
||||
// This would integrate with other services to check data retention
|
||||
// For now, we'll check audit logs as an example
|
||||
const deletedCount = await AuditLog.cleanupExpired();
|
||||
|
||||
if (deletedCount > 0) {
|
||||
logger.info(`Cleaned up ${deletedCount} expired audit logs`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Retention policy check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for compliance violations
|
||||
*/
|
||||
async checkViolations() {
|
||||
try {
|
||||
const activeViolations = await ComplianceViolation.findActive();
|
||||
|
||||
for (const violation of activeViolations) {
|
||||
// Update risk scores
|
||||
violation.calculateRiskScore();
|
||||
|
||||
// Check if needs escalation
|
||||
if (violation.needsEscalation() && violation.status !== 'escalated') {
|
||||
await this.escalateViolation(violation);
|
||||
}
|
||||
|
||||
// Check if overdue
|
||||
if (violation.isOverdue()) {
|
||||
await this.handleOverdueViolation(violation);
|
||||
}
|
||||
|
||||
await violation.save();
|
||||
}
|
||||
|
||||
// Check for violations requiring notification
|
||||
const notificationRequired = await ComplianceViolation.findRequiringNotification();
|
||||
|
||||
for (const violation of notificationRequired) {
|
||||
await this.notifyAuthorities(violation);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Violation check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create compliance violation
|
||||
*/
|
||||
async createViolation(violationData, accountId) {
|
||||
const violation = new ComplianceViolation({
|
||||
violationId: `vio_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
accountId,
|
||||
...violationData,
|
||||
timeline: {
|
||||
detectedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate initial risk score
|
||||
violation.calculateRiskScore();
|
||||
|
||||
// Set deadlines based on regulation
|
||||
this.setViolationDeadlines(violation);
|
||||
|
||||
await violation.save();
|
||||
|
||||
await publishEvent('violation.detected', {
|
||||
violationId: violation.violationId,
|
||||
type: violation.type,
|
||||
severity: violation.severity,
|
||||
riskScore: violation.risk.score
|
||||
});
|
||||
|
||||
return violation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set violation deadlines based on regulation
|
||||
*/
|
||||
setViolationDeadlines(violation) {
|
||||
const regulation = violation.details.regulation;
|
||||
|
||||
switch (regulation) {
|
||||
case 'gdpr':
|
||||
// GDPR: 72 hours for breach notification
|
||||
if (violation.type === 'security_breach') {
|
||||
violation.timeline.deadlines.notification = new Date(Date.now() + 72 * 60 * 60 * 1000);
|
||||
}
|
||||
violation.timeline.deadlines.remediation = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
|
||||
case 'ccpa':
|
||||
// CCPA: 30 days for data requests
|
||||
violation.timeline.deadlines.remediation = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Default: 30 days
|
||||
violation.timeline.deadlines.remediation = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escalate violation
|
||||
*/
|
||||
async escalateViolation(violation) {
|
||||
violation.status = 'escalated';
|
||||
await violation.save();
|
||||
|
||||
await publishEvent('violation.escalated', {
|
||||
violationId: violation.violationId,
|
||||
type: violation.type,
|
||||
severity: violation.severity,
|
||||
riskScore: violation.risk.score,
|
||||
reason: 'High risk or overdue'
|
||||
});
|
||||
|
||||
// Notify management
|
||||
logger.error(`ESCALATED VIOLATION: ${violation.violationId} - ${violation.type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle overdue violation
|
||||
*/
|
||||
async handleOverdueViolation(violation) {
|
||||
// Update risk score
|
||||
violation.risk.score = Math.min(10, violation.risk.score * 1.5);
|
||||
|
||||
await publishEvent('violation.overdue', {
|
||||
violationId: violation.violationId,
|
||||
type: violation.type,
|
||||
daysOverdue: Math.ceil((new Date() - violation.timeline.deadlines.remediation) / (1000 * 60 * 60 * 24))
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify authorities about violation
|
||||
*/
|
||||
async notifyAuthorities(violation) {
|
||||
// In production, this would send actual notifications
|
||||
logger.info(`Notifying authorities about violation ${violation.violationId}`);
|
||||
|
||||
violation.notifications.authorities.push({
|
||||
name: 'Data Protection Authority',
|
||||
notifiedAt: new Date(),
|
||||
method: 'automated',
|
||||
reference: `DPA-${violation.violationId}`
|
||||
});
|
||||
|
||||
await violation.save();
|
||||
|
||||
await publishEvent('violation.authority_notified', {
|
||||
violationId: violation.violationId,
|
||||
authority: 'Data Protection Authority'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate compliance report
|
||||
*/
|
||||
async generateComplianceReport() {
|
||||
try {
|
||||
const report = {
|
||||
date: new Date(),
|
||||
period: 'daily',
|
||||
violations: await ComplianceViolation.getViolationStats(null, 1),
|
||||
dataRequests: await DataRequest.getRequestStats(null,
|
||||
new Date(Date.now() - 24 * 60 * 60 * 1000),
|
||||
new Date()
|
||||
),
|
||||
consents: await this.getConsentStats(),
|
||||
auditActivity: await AuditLog.getActivitySummary(null, 1)
|
||||
};
|
||||
|
||||
await publishEvent('compliance.report_generated', report);
|
||||
|
||||
logger.info('Daily compliance report generated');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Report generation failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get consent statistics
|
||||
*/
|
||||
async getConsentStats() {
|
||||
const stats = await ConsentRecord.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
type: '$type',
|
||||
status: '$status'
|
||||
},
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$_id.type',
|
||||
statuses: {
|
||||
$push: {
|
||||
status: '$_id.status',
|
||||
count: '$count'
|
||||
}
|
||||
},
|
||||
total: { $sum: '$count' }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get regulation by region
|
||||
*/
|
||||
getRegulationByRegion(country) {
|
||||
if (!country) return 'other';
|
||||
|
||||
for (const [regulation, countries] of Object.entries(config.compliance.regions)) {
|
||||
if (countries.includes(country)) {
|
||||
return regulation;
|
||||
}
|
||||
}
|
||||
|
||||
return 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check user location for compliance
|
||||
*/
|
||||
checkUserLocation(ipAddress) {
|
||||
const geo = geoip.lookup(ipAddress);
|
||||
if (!geo) return { country: 'unknown', regulation: 'other' };
|
||||
|
||||
const country = geo.country;
|
||||
const regulation = this.getRegulationByRegion(country);
|
||||
|
||||
return {
|
||||
country,
|
||||
region: geo.region,
|
||||
city: geo.city,
|
||||
regulation,
|
||||
requiresConsent: ['gdpr', 'ccpa'].includes(regulation)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import crypto from 'crypto';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { config } from '../config/index.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
export class DataProtectionService {
|
||||
constructor() {
|
||||
this.algorithm = config.encryption.algorithm;
|
||||
this.keyDerivation = config.encryption.keyDerivation;
|
||||
this.saltRounds = config.encryption.saltRounds;
|
||||
this.masterKey = this.deriveMasterKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive master key from configuration
|
||||
*/
|
||||
deriveMasterKey() {
|
||||
const salt = crypto.createHash('sha256').update('compliance-guard-salt').digest();
|
||||
return crypto.pbkdf2Sync(
|
||||
config.encryption.encryptionKey,
|
||||
salt,
|
||||
100000,
|
||||
32,
|
||||
'sha256'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt sensitive data
|
||||
*/
|
||||
encrypt(data, context = {}) {
|
||||
try {
|
||||
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
|
||||
// Generate unique IV for each encryption
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(this.algorithm, this.masterKey, iv);
|
||||
|
||||
let encrypted = cipher.update(dataStr, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Combine IV, authTag, and encrypted data
|
||||
const result = {
|
||||
iv: iv.toString('hex'),
|
||||
authTag: authTag.toString('hex'),
|
||||
data: encrypted,
|
||||
algorithm: this.algorithm,
|
||||
timestamp: new Date().toISOString(),
|
||||
context
|
||||
};
|
||||
|
||||
return Buffer.from(JSON.stringify(result)).toString('base64');
|
||||
} catch (error) {
|
||||
logger.error('Encryption failed:', error);
|
||||
throw new Error('Failed to encrypt data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt sensitive data
|
||||
*/
|
||||
decrypt(encryptedData) {
|
||||
try {
|
||||
const parsed = JSON.parse(Buffer.from(encryptedData, 'base64').toString());
|
||||
|
||||
const decipher = crypto.createDecipheriv(
|
||||
parsed.algorithm || this.algorithm,
|
||||
this.masterKey,
|
||||
Buffer.from(parsed.iv, 'hex')
|
||||
);
|
||||
|
||||
if (parsed.authTag) {
|
||||
decipher.setAuthTag(Buffer.from(parsed.authTag, 'hex'));
|
||||
}
|
||||
|
||||
let decrypted = decipher.update(parsed.data, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
try {
|
||||
return JSON.parse(decrypted);
|
||||
} catch {
|
||||
return decrypted;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Decryption failed:', error);
|
||||
throw new Error('Failed to decrypt data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash data for integrity checking
|
||||
*/
|
||||
hash(data) {
|
||||
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
return crypto.createHash('sha256').update(dataStr).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate secure token
|
||||
*/
|
||||
generateToken(length = 32) {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymize personal data
|
||||
*/
|
||||
anonymize(data, fields = []) {
|
||||
const anonymized = { ...data };
|
||||
|
||||
const defaultFields = ['email', 'phone', 'name', 'address', 'ip'];
|
||||
const fieldsToAnonymize = fields.length > 0 ? fields : defaultFields;
|
||||
|
||||
for (const field of fieldsToAnonymize) {
|
||||
if (anonymized[field]) {
|
||||
anonymized[field] = this.anonymizeField(anonymized[field], field);
|
||||
}
|
||||
}
|
||||
|
||||
return anonymized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymize specific field based on type
|
||||
*/
|
||||
anonymizeField(value, fieldType) {
|
||||
switch (fieldType) {
|
||||
case 'email':
|
||||
const [localPart, domain] = value.split('@');
|
||||
const maskedLocal = localPart.substring(0, 2) + '***';
|
||||
return `${maskedLocal}@${domain}`;
|
||||
|
||||
case 'phone':
|
||||
return value.substring(0, 3) + '****' + value.substring(value.length - 2);
|
||||
|
||||
case 'name':
|
||||
const parts = value.split(' ');
|
||||
return parts.map(part => part.charAt(0) + '***').join(' ');
|
||||
|
||||
case 'ip':
|
||||
const octets = value.split('.');
|
||||
return octets.length === 4
|
||||
? `${octets[0]}.${octets[1]}.XXX.XXX`
|
||||
: 'XXX.XXX.XXX.XXX';
|
||||
|
||||
default:
|
||||
// Generic masking
|
||||
const visibleLength = Math.min(3, Math.floor(value.length * 0.3));
|
||||
return value.substring(0, visibleLength) + '*'.repeat(value.length - visibleLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pseudonymize data (reversible anonymization)
|
||||
*/
|
||||
pseudonymize(data, fields = []) {
|
||||
const pseudonymized = { ...data };
|
||||
const mapping = {};
|
||||
|
||||
for (const field of fields) {
|
||||
if (pseudonymized[field]) {
|
||||
const pseudonym = this.generatePseudonym(pseudonymized[field]);
|
||||
mapping[pseudonym] = pseudonymized[field];
|
||||
pseudonymized[field] = pseudonym;
|
||||
}
|
||||
}
|
||||
|
||||
return { data: pseudonymized, mapping };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate pseudonym
|
||||
*/
|
||||
generatePseudonym(value) {
|
||||
const hash = crypto.createHash('sha256').update(value + this.masterKey).digest('hex');
|
||||
return `PSEUDO_${hash.substring(0, 16)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize sensitive data
|
||||
*/
|
||||
tokenize(data) {
|
||||
const token = this.generateToken();
|
||||
const encrypted = this.encrypt(data);
|
||||
|
||||
// Store token-data mapping (in production, use secure token vault)
|
||||
return {
|
||||
token,
|
||||
encrypted,
|
||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if data should be encrypted based on field type
|
||||
*/
|
||||
shouldEncrypt(fieldName) {
|
||||
const sensitiveFields = [
|
||||
'password', 'ssn', 'creditCard', 'bankAccount',
|
||||
'medicalRecord', 'biometric', 'passport', 'driverLicense'
|
||||
];
|
||||
|
||||
return sensitiveFields.some(field =>
|
||||
fieldName.toLowerCase().includes(field.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure erase data
|
||||
*/
|
||||
secureErase(data) {
|
||||
if (typeof data === 'string') {
|
||||
// Overwrite string in memory (limited effectiveness in JS)
|
||||
const buffer = Buffer.from(data);
|
||||
crypto.randomFillSync(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof data === 'object') {
|
||||
for (const key in data) {
|
||||
if (typeof data[key] === 'string') {
|
||||
const buffer = Buffer.from(data[key]);
|
||||
crypto.randomFillSync(buffer);
|
||||
}
|
||||
delete data[key];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate data retention policy
|
||||
*/
|
||||
generateRetentionPolicy(dataType, region) {
|
||||
const policies = config.compliance.retentionPeriods;
|
||||
let retentionDays = policies.default;
|
||||
|
||||
// Apply region-specific rules
|
||||
if (config.compliance.regions.gdpr.includes(region)) {
|
||||
retentionDays = policies.eu;
|
||||
} else if (region === 'US') {
|
||||
retentionDays = policies.us;
|
||||
}
|
||||
|
||||
// Apply data type specific rules
|
||||
if (dataType === 'marketing') {
|
||||
retentionDays = Math.min(retentionDays, policies.marketing);
|
||||
} else if (dataType === 'analytics') {
|
||||
retentionDays = Math.min(retentionDays, policies.analytics);
|
||||
}
|
||||
|
||||
return {
|
||||
retentionDays,
|
||||
expiryDate: new Date(Date.now() + retentionDays * 24 * 60 * 60 * 1000),
|
||||
policy: `${dataType}_${region}_${retentionDays}d`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Export data in portable format
|
||||
*/
|
||||
exportUserData(userData, format = 'json') {
|
||||
const exportData = {
|
||||
exportDate: new Date().toISOString(),
|
||||
format,
|
||||
version: '1.0',
|
||||
data: userData
|
||||
};
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return JSON.stringify(exportData, null, 2);
|
||||
|
||||
case 'csv':
|
||||
return this.convertToCSV(userData);
|
||||
|
||||
case 'xml':
|
||||
return this.convertToXML(exportData);
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported export format: ${format}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert data to CSV format
|
||||
*/
|
||||
convertToCSV(data) {
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length === 0) return '';
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
const csvHeaders = headers.join(',');
|
||||
|
||||
const csvRows = data.map(row =>
|
||||
headers.map(header => {
|
||||
const value = row[header];
|
||||
return typeof value === 'string' && value.includes(',')
|
||||
? `"${value}"`
|
||||
: value;
|
||||
}).join(',')
|
||||
);
|
||||
|
||||
return [csvHeaders, ...csvRows].join('\n');
|
||||
}
|
||||
|
||||
// Convert single object to CSV
|
||||
const headers = Object.keys(data);
|
||||
const values = headers.map(h => data[h]);
|
||||
return [headers.join(','), values.join(',')].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert data to XML format
|
||||
*/
|
||||
convertToXML(data) {
|
||||
const convert = (obj, rootName = 'root') => {
|
||||
let xml = `<${rootName}>`;
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
xml += Array.isArray(value)
|
||||
? value.map(item => convert(item, key)).join('')
|
||||
: convert(value, key);
|
||||
} else {
|
||||
xml += `<${key}>${value}</${key}>`;
|
||||
}
|
||||
}
|
||||
|
||||
xml += `</${rootName}>`;
|
||||
return xml;
|
||||
};
|
||||
|
||||
return '<?xml version="1.0" encoding="UTF-8"?>' + convert(data, 'export');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { DataRequest } from '../models/DataRequest.js';
|
||||
import { AuditLog } from '../models/AuditLog.js';
|
||||
import { DataProtectionService } from './dataProtection.js';
|
||||
import { publishEvent } from './messaging.js';
|
||||
import { config } from '../config/index.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
export class DataRequestProcessor {
|
||||
constructor(redisClient) {
|
||||
this.redis = redisClient;
|
||||
this.dataProtection = new DataProtectionService();
|
||||
this.processingQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new data request
|
||||
*/
|
||||
async createDataRequest(requestData) {
|
||||
const requestId = `req_${Date.now()}_${uuidv4().substring(0, 8)}`;
|
||||
|
||||
// Determine SLA based on request type and regulation
|
||||
const sla = this.determineSLA(requestData.type, requestData.regulation);
|
||||
|
||||
const dataRequest = new DataRequest({
|
||||
requestId,
|
||||
...requestData,
|
||||
compliance: {
|
||||
regulation: requestData.regulation || 'gdpr',
|
||||
deadline: new Date(Date.now() + sla * 24 * 60 * 60 * 1000),
|
||||
sla,
|
||||
articles: this.getRelevantArticles(requestData.type, requestData.regulation)
|
||||
}
|
||||
});
|
||||
|
||||
await dataRequest.save();
|
||||
|
||||
// Log the request
|
||||
await AuditLog.log({
|
||||
accountId: requestData.accountId,
|
||||
userId: requestData.userId,
|
||||
action: 'data_request_created',
|
||||
category: 'data_access',
|
||||
resource: 'data_request',
|
||||
resourceId: requestId,
|
||||
details: {
|
||||
type: requestData.type,
|
||||
regulation: requestData.regulation
|
||||
},
|
||||
result: { status: 'success' },
|
||||
actor: {
|
||||
type: 'user',
|
||||
id: requestData.userId,
|
||||
ip: requestData.audit?.ipAddress
|
||||
}
|
||||
});
|
||||
|
||||
await publishEvent('data_request.created', {
|
||||
requestId,
|
||||
type: requestData.type,
|
||||
userId: requestData.userId
|
||||
});
|
||||
|
||||
return dataRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify data request
|
||||
*/
|
||||
async verifyRequest(requestId, verificationData) {
|
||||
const request = await DataRequest.findOne({ requestId });
|
||||
if (!request) {
|
||||
throw new Error('Data request not found');
|
||||
}
|
||||
|
||||
request.verification.verified = true;
|
||||
request.verification.verifiedAt = new Date();
|
||||
request.verification.verifiedBy = verificationData.verifiedBy;
|
||||
request.verification.method = verificationData.method;
|
||||
|
||||
await request.save();
|
||||
|
||||
await publishEvent('data_request.verified', {
|
||||
requestId,
|
||||
userId: request.userId
|
||||
});
|
||||
|
||||
// Auto-process if verified
|
||||
if (request.canProcess()) {
|
||||
await this.processRequest(requestId);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process data request
|
||||
*/
|
||||
async processRequest(requestId) {
|
||||
const request = await DataRequest.findOne({ requestId });
|
||||
if (!request) {
|
||||
throw new Error('Data request not found');
|
||||
}
|
||||
|
||||
if (!request.canProcess()) {
|
||||
throw new Error('Request cannot be processed in current state');
|
||||
}
|
||||
|
||||
try {
|
||||
request.markProcessing('system');
|
||||
await request.save();
|
||||
|
||||
// Collect data based on request type
|
||||
const collectedData = await this.collectUserData(request);
|
||||
|
||||
// Process based on request type
|
||||
let responseData;
|
||||
switch (request.type) {
|
||||
case 'access':
|
||||
responseData = await this.handleAccessRequest(request, collectedData);
|
||||
break;
|
||||
|
||||
case 'portability':
|
||||
responseData = await this.handlePortabilityRequest(request, collectedData);
|
||||
break;
|
||||
|
||||
case 'deletion':
|
||||
responseData = await this.handleDeletionRequest(request, collectedData);
|
||||
break;
|
||||
|
||||
case 'rectification':
|
||||
responseData = await this.handleRectificationRequest(request, collectedData);
|
||||
break;
|
||||
|
||||
case 'restriction':
|
||||
responseData = await this.handleRestrictionRequest(request, collectedData);
|
||||
break;
|
||||
|
||||
case 'objection':
|
||||
responseData = await this.handleObjectionRequest(request, collectedData);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown request type: ${request.type}`);
|
||||
}
|
||||
|
||||
request.markCompleted(responseData);
|
||||
await request.save();
|
||||
|
||||
await publishEvent('data_request.completed', {
|
||||
requestId,
|
||||
type: request.type,
|
||||
userId: request.userId
|
||||
});
|
||||
|
||||
// Log completion
|
||||
await AuditLog.log({
|
||||
accountId: request.accountId,
|
||||
userId: request.userId,
|
||||
action: 'data_request_completed',
|
||||
category: 'data_access',
|
||||
resource: 'data_request',
|
||||
resourceId: requestId,
|
||||
details: {
|
||||
type: request.type,
|
||||
processingTime: request.processing.completedAt - request.processing.startedAt
|
||||
},
|
||||
result: { status: 'success' },
|
||||
actor: { type: 'system', id: 'data-processor' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Request processing failed:', error);
|
||||
|
||||
request.processing.errors.push({
|
||||
timestamp: new Date(),
|
||||
message: error.message,
|
||||
service: 'data-processor'
|
||||
});
|
||||
|
||||
request.status = 'pending'; // Reset to pending
|
||||
await request.save();
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle access request
|
||||
*/
|
||||
async handleAccessRequest(request, collectedData) {
|
||||
// Format data for user access
|
||||
const formattedData = this.formatDataForExport(
|
||||
collectedData,
|
||||
request.requestDetails.format
|
||||
);
|
||||
|
||||
// Generate download URL
|
||||
const downloadInfo = await this.generateDownloadLink(
|
||||
formattedData,
|
||||
request.requestDetails.format,
|
||||
request.requestId
|
||||
);
|
||||
|
||||
return {
|
||||
method: 'download',
|
||||
deliveredAt: new Date(),
|
||||
downloadUrl: downloadInfo.url,
|
||||
downloadExpiry: downloadInfo.expiry
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle portability request
|
||||
*/
|
||||
async handlePortabilityRequest(request, collectedData) {
|
||||
// Export in machine-readable format
|
||||
const exportedData = this.dataProtection.exportUserData(
|
||||
collectedData,
|
||||
request.requestDetails.format
|
||||
);
|
||||
|
||||
const downloadInfo = await this.generateDownloadLink(
|
||||
exportedData,
|
||||
request.requestDetails.format,
|
||||
request.requestId
|
||||
);
|
||||
|
||||
return {
|
||||
method: 'download',
|
||||
deliveredAt: new Date(),
|
||||
downloadUrl: downloadInfo.url,
|
||||
downloadExpiry: downloadInfo.expiry
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle deletion request
|
||||
*/
|
||||
async handleDeletionRequest(request, collectedData) {
|
||||
const deletionResults = [];
|
||||
|
||||
// Delete data from each service
|
||||
for (const category of request.requestDetails.dataCategories || ['all']) {
|
||||
try {
|
||||
const result = await this.deleteUserData(
|
||||
request.userId,
|
||||
request.accountId,
|
||||
category
|
||||
);
|
||||
|
||||
deletionResults.push({
|
||||
category,
|
||||
status: 'deleted',
|
||||
recordsDeleted: result.count
|
||||
});
|
||||
} catch (error) {
|
||||
deletionResults.push({
|
||||
category,
|
||||
status: 'failed',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Anonymize remaining data that cannot be deleted
|
||||
await this.anonymizeRemainingData(request.userId, request.accountId);
|
||||
|
||||
return {
|
||||
method: 'api',
|
||||
deliveredAt: new Date(),
|
||||
deletionResults
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rectification request
|
||||
*/
|
||||
async handleRectificationRequest(request, collectedData) {
|
||||
// Apply corrections
|
||||
const corrections = request.requestDetails.corrections || {};
|
||||
const results = [];
|
||||
|
||||
for (const [field, newValue] of Object.entries(corrections)) {
|
||||
try {
|
||||
await this.updateUserData(
|
||||
request.userId,
|
||||
request.accountId,
|
||||
field,
|
||||
newValue
|
||||
);
|
||||
|
||||
results.push({
|
||||
field,
|
||||
status: 'corrected',
|
||||
oldValue: collectedData[field],
|
||||
newValue
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
field,
|
||||
status: 'failed',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
method: 'api',
|
||||
deliveredAt: new Date(),
|
||||
corrections: results
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle restriction request
|
||||
*/
|
||||
async handleRestrictionRequest(request, collectedData) {
|
||||
// Restrict processing for specified purposes
|
||||
const restrictions = request.requestDetails.restrictions || [];
|
||||
|
||||
for (const restriction of restrictions) {
|
||||
await this.applyProcessingRestriction(
|
||||
request.userId,
|
||||
request.accountId,
|
||||
restriction
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
method: 'api',
|
||||
deliveredAt: new Date(),
|
||||
restrictionsApplied: restrictions
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle objection request
|
||||
*/
|
||||
async handleObjectionRequest(request, collectedData) {
|
||||
// Record objection and stop processing
|
||||
const objections = request.requestDetails.objections || [];
|
||||
|
||||
for (const objection of objections) {
|
||||
await this.recordProcessingObjection(
|
||||
request.userId,
|
||||
request.accountId,
|
||||
objection
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
method: 'api',
|
||||
deliveredAt: new Date(),
|
||||
objectionsRecorded: objections
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect user data from all services
|
||||
*/
|
||||
async collectUserData(request) {
|
||||
const { userId, accountId } = request;
|
||||
const scope = request.requestDetails.scope;
|
||||
const categories = request.requestDetails.dataCategories || ['all'];
|
||||
|
||||
// This would integrate with all services to collect data
|
||||
const collectedData = {
|
||||
profile: await this.getProfileData(userId, accountId),
|
||||
activities: await this.getActivityData(userId, accountId),
|
||||
messages: await this.getMessageData(userId, accountId),
|
||||
analytics: await this.getAnalyticsData(userId, accountId),
|
||||
consents: await this.getConsentData(userId, accountId),
|
||||
metadata: {
|
||||
collectionDate: new Date(),
|
||||
userId,
|
||||
accountId
|
||||
}
|
||||
};
|
||||
|
||||
// Filter by categories if specified
|
||||
if (scope === 'specific' && !categories.includes('all')) {
|
||||
const filteredData = {};
|
||||
for (const category of categories) {
|
||||
if (collectedData[category]) {
|
||||
filteredData[category] = collectedData[category];
|
||||
}
|
||||
}
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
return collectedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock data collection methods (would integrate with actual services)
|
||||
*/
|
||||
async getProfileData(userId, accountId) {
|
||||
// Integrate with user service
|
||||
return {
|
||||
userId,
|
||||
accountId,
|
||||
createdAt: new Date(),
|
||||
// ... other profile data
|
||||
};
|
||||
}
|
||||
|
||||
async getActivityData(userId, accountId) {
|
||||
// Integrate with analytics service
|
||||
return [];
|
||||
}
|
||||
|
||||
async getMessageData(userId, accountId) {
|
||||
// Integrate with messaging service
|
||||
return [];
|
||||
}
|
||||
|
||||
async getAnalyticsData(userId, accountId) {
|
||||
// Integrate with analytics service
|
||||
return {};
|
||||
}
|
||||
|
||||
async getConsentData(userId, accountId) {
|
||||
// Get from consent records
|
||||
const ConsentRecord = require('../models/ConsentRecord').ConsentRecord;
|
||||
return await ConsentRecord.find({ userId, accountId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Format data for export
|
||||
*/
|
||||
formatDataForExport(data, format) {
|
||||
switch (format) {
|
||||
case 'json':
|
||||
return JSON.stringify(data, null, 2);
|
||||
case 'csv':
|
||||
return this.dataProtection.convertToCSV(data);
|
||||
case 'xml':
|
||||
return this.dataProtection.convertToXML(data);
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate download link
|
||||
*/
|
||||
async generateDownloadLink(data, format, requestId) {
|
||||
// In production, this would upload to secure storage
|
||||
const token = this.dataProtection.generateToken();
|
||||
const expiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
||||
|
||||
// Store encrypted data with token
|
||||
await this.redis.setex(
|
||||
`download:${token}`,
|
||||
7 * 24 * 60 * 60, // 7 days
|
||||
this.dataProtection.encrypt({
|
||||
data,
|
||||
format,
|
||||
requestId
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
url: `/api/download/${token}`,
|
||||
expiry,
|
||||
token
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user data
|
||||
*/
|
||||
async deleteUserData(userId, accountId, category) {
|
||||
// This would integrate with all services to delete data
|
||||
logger.info(`Deleting ${category} data for user ${userId}`);
|
||||
|
||||
// Return mock result
|
||||
return {
|
||||
category,
|
||||
count: Math.floor(Math.random() * 100) + 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymize remaining data
|
||||
*/
|
||||
async anonymizeRemainingData(userId, accountId) {
|
||||
logger.info(`Anonymizing remaining data for user ${userId}`);
|
||||
// This would integrate with services to anonymize data
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user data
|
||||
*/
|
||||
async updateUserData(userId, accountId, field, newValue) {
|
||||
logger.info(`Updating ${field} for user ${userId}`);
|
||||
// This would integrate with services to update data
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply processing restriction
|
||||
*/
|
||||
async applyProcessingRestriction(userId, accountId, restriction) {
|
||||
logger.info(`Applying restriction ${restriction.type} for user ${userId}`);
|
||||
// This would integrate with services to restrict processing
|
||||
}
|
||||
|
||||
/**
|
||||
* Record processing objection
|
||||
*/
|
||||
async recordProcessingObjection(userId, accountId, objection) {
|
||||
logger.info(`Recording objection ${objection.type} for user ${userId}`);
|
||||
// This would integrate with services to stop processing
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine SLA based on request type and regulation
|
||||
*/
|
||||
determineSLA(type, regulation) {
|
||||
const slaMap = {
|
||||
gdpr: { default: 30, deletion: 30, access: 30 },
|
||||
ccpa: { default: 45, deletion: 45, access: 45 },
|
||||
lgpd: { default: 15, deletion: 20, access: 15 }
|
||||
};
|
||||
|
||||
const regulationSLA = slaMap[regulation] || slaMap.gdpr;
|
||||
return regulationSLA[type] || regulationSLA.default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relevant articles based on request type
|
||||
*/
|
||||
getRelevantArticles(type, regulation) {
|
||||
const articleMap = {
|
||||
gdpr: {
|
||||
access: ['Article 15'],
|
||||
portability: ['Article 20'],
|
||||
deletion: ['Article 17'],
|
||||
rectification: ['Article 16'],
|
||||
restriction: ['Article 18'],
|
||||
objection: ['Article 21']
|
||||
},
|
||||
ccpa: {
|
||||
access: ['Section 1798.100'],
|
||||
deletion: ['Section 1798.105'],
|
||||
opt_out: ['Section 1798.120']
|
||||
}
|
||||
};
|
||||
|
||||
const regulationArticles = articleMap[regulation] || articleMap.gdpr;
|
||||
return regulationArticles[type] || [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import amqp from 'amqplib';
|
||||
import { config } from '../config/index.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
let connection = null;
|
||||
let channel = null;
|
||||
|
||||
/**
|
||||
* Connect to RabbitMQ
|
||||
*/
|
||||
export const connectRabbitMQ = async () => {
|
||||
try {
|
||||
connection = await amqp.connect(config.rabbitmq.url);
|
||||
channel = await connection.createChannel();
|
||||
|
||||
// Create exchange
|
||||
await channel.assertExchange(config.rabbitmq.exchange, 'topic', {
|
||||
durable: true
|
||||
});
|
||||
|
||||
// Create queues
|
||||
for (const [name, queueName] of Object.entries(config.rabbitmq.queues)) {
|
||||
await channel.assertQueue(queueName, { durable: true });
|
||||
}
|
||||
|
||||
logger.info('Connected to RabbitMQ');
|
||||
|
||||
// Handle connection events
|
||||
connection.on('error', (err) => {
|
||||
logger.error('RabbitMQ connection error:', err);
|
||||
});
|
||||
|
||||
connection.on('close', () => {
|
||||
logger.error('RabbitMQ connection closed, reconnecting...');
|
||||
setTimeout(connectRabbitMQ, 5000);
|
||||
});
|
||||
|
||||
return channel;
|
||||
} catch (error) {
|
||||
logger.error('Failed to connect to RabbitMQ:', error);
|
||||
setTimeout(connectRabbitMQ, 5000);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Publish event to RabbitMQ
|
||||
*/
|
||||
export const publishEvent = async (eventType, data) => {
|
||||
try {
|
||||
if (!channel) {
|
||||
await connectRabbitMQ();
|
||||
}
|
||||
|
||||
const message = {
|
||||
eventType,
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'compliance-guard'
|
||||
};
|
||||
|
||||
const routingKey = `compliance.${eventType}`;
|
||||
|
||||
channel.publish(
|
||||
config.rabbitmq.exchange,
|
||||
routingKey,
|
||||
Buffer.from(JSON.stringify(message)),
|
||||
{ persistent: true }
|
||||
);
|
||||
|
||||
logger.debug(`Published event: ${eventType}`, { data });
|
||||
} catch (error) {
|
||||
logger.error('Failed to publish event:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe to events
|
||||
*/
|
||||
export const subscribeToEvents = async (patterns, handler) => {
|
||||
try {
|
||||
if (!channel) {
|
||||
await connectRabbitMQ();
|
||||
}
|
||||
|
||||
// Create exclusive queue for this consumer
|
||||
const { queue } = await channel.assertQueue('', { exclusive: true });
|
||||
|
||||
// Bind patterns
|
||||
for (const pattern of patterns) {
|
||||
await channel.bindQueue(queue, config.rabbitmq.exchange, pattern);
|
||||
}
|
||||
|
||||
// Consume messages
|
||||
channel.consume(queue, async (msg) => {
|
||||
if (!msg) return;
|
||||
|
||||
try {
|
||||
const message = JSON.parse(msg.content.toString());
|
||||
await handler(message);
|
||||
channel.ack(msg);
|
||||
} catch (error) {
|
||||
logger.error('Error processing message:', error);
|
||||
channel.nack(msg, false, false); // Don't requeue
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`Subscribed to patterns: ${patterns.join(', ')}`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to subscribe to events:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Close RabbitMQ connection
|
||||
*/
|
||||
export const closeConnection = async () => {
|
||||
try {
|
||||
if (channel) await channel.close();
|
||||
if (connection) await connection.close();
|
||||
logger.info('RabbitMQ connection closed');
|
||||
} catch (error) {
|
||||
logger.error('Error closing RabbitMQ connection:', error);
|
||||
}
|
||||
};
|
||||
217
marketing-agent/services/compliance-guard/src/utils/auth.js
Normal file
217
marketing-agent/services/compliance-guard/src/utils/auth.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { config } from '../config/index.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* JWT authentication middleware
|
||||
*/
|
||||
export const auth = (requiredRole = null) => {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
// Extract token from header
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'No token provided'
|
||||
});
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
// Verify token
|
||||
const decoded = jwt.verify(token, config.jwt.secret);
|
||||
|
||||
// Check token expiration
|
||||
if (decoded.exp && decoded.exp < Date.now() / 1000) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Token expired'
|
||||
});
|
||||
}
|
||||
|
||||
// Check required role if specified
|
||||
if (requiredRole && decoded.role !== requiredRole && decoded.role !== 'admin') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Insufficient permissions'
|
||||
});
|
||||
}
|
||||
|
||||
// Attach user info to request
|
||||
req.user = {
|
||||
id: decoded.userId,
|
||||
accountId: decoded.accountId,
|
||||
role: decoded.role,
|
||||
permissions: decoded.permissions || []
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('Authentication error:', error);
|
||||
|
||||
if (error.name === 'JsonWebTokenError') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid token'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Token expired'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Authentication failed'
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* API key authentication middleware
|
||||
*/
|
||||
export const apiKeyAuth = () => {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'API key required'
|
||||
});
|
||||
}
|
||||
|
||||
// In production, validate against database
|
||||
if (apiKey !== config.apiKeys.internal) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid API key'
|
||||
});
|
||||
}
|
||||
|
||||
// Set service context
|
||||
req.service = {
|
||||
name: 'internal-service',
|
||||
permissions: ['read', 'write']
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('API key authentication error:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Authentication failed'
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Permission check middleware
|
||||
*/
|
||||
export const requirePermission = (permission) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Authentication required'
|
||||
});
|
||||
}
|
||||
|
||||
const hasPermission = req.user.role === 'admin' ||
|
||||
(req.user.permissions && req.user.permissions.includes(permission));
|
||||
|
||||
if (!hasPermission) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: `Permission '${permission}' required`
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Rate limiting by user
|
||||
*/
|
||||
export const userRateLimit = (maxRequests = 100, windowMs = 60000) => {
|
||||
const requests = new Map();
|
||||
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const userId = req.user.id;
|
||||
const now = Date.now();
|
||||
const userRequests = requests.get(userId) || [];
|
||||
|
||||
// Clean old requests
|
||||
const validRequests = userRequests.filter(time => now - time < windowMs);
|
||||
|
||||
if (validRequests.length >= maxRequests) {
|
||||
return res.status(429).json({
|
||||
success: false,
|
||||
error: 'Too many requests',
|
||||
retryAfter: Math.ceil(windowMs / 1000)
|
||||
});
|
||||
}
|
||||
|
||||
validRequests.push(now);
|
||||
requests.set(userId, validRequests);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate JWT token
|
||||
*/
|
||||
export const generateToken = (payload, expiresIn = '24h') => {
|
||||
return jwt.sign(payload, config.jwt.secret, {
|
||||
expiresIn,
|
||||
issuer: 'compliance-guard',
|
||||
audience: 'marketing-agent'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify JWT token
|
||||
*/
|
||||
export const verifyToken = (token) => {
|
||||
return jwt.verify(token, config.jwt.secret, {
|
||||
issuer: 'compliance-guard',
|
||||
audience: 'marketing-agent'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hash API key
|
||||
*/
|
||||
export const hashApiKey = (apiKey) => {
|
||||
const crypto = require('crypto');
|
||||
return crypto.createHash('sha256').update(apiKey).digest('hex');
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate request signature
|
||||
*/
|
||||
export const validateSignature = (payload, signature, secret) => {
|
||||
const crypto = require('crypto');
|
||||
const expectedSignature = crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(JSON.stringify(payload))
|
||||
.digest('hex');
|
||||
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(expectedSignature)
|
||||
);
|
||||
};
|
||||
162
marketing-agent/services/compliance-guard/src/utils/logger.js
Normal file
162
marketing-agent/services/compliance-guard/src/utils/logger.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import winston from 'winston';
|
||||
import DailyRotateFile from 'winston-daily-rotate-file';
|
||||
import { config } from '../config/index.js';
|
||||
|
||||
const { combine, timestamp, json, printf, colorize, errors } = winston.format;
|
||||
|
||||
// Custom format for console output
|
||||
const consoleFormat = printf(({ level, message, timestamp, ...metadata }) => {
|
||||
let msg = `${timestamp} [${level}]: ${message}`;
|
||||
if (Object.keys(metadata).length > 0) {
|
||||
msg += ` ${JSON.stringify(metadata)}`;
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
|
||||
// Create transport for daily rotate file
|
||||
const fileRotateTransport = new DailyRotateFile({
|
||||
filename: 'logs/compliance-guard-%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d',
|
||||
format: combine(
|
||||
timestamp(),
|
||||
errors({ stack: true }),
|
||||
json()
|
||||
)
|
||||
});
|
||||
|
||||
// Create transport for error logs
|
||||
const errorFileTransport = new DailyRotateFile({
|
||||
level: 'error',
|
||||
filename: 'logs/compliance-guard-error-%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: '20m',
|
||||
maxFiles: '30d',
|
||||
format: combine(
|
||||
timestamp(),
|
||||
errors({ stack: true }),
|
||||
json()
|
||||
)
|
||||
});
|
||||
|
||||
// Create transport for audit logs (separate from application logs)
|
||||
const auditFileTransport = new DailyRotateFile({
|
||||
filename: 'logs/audit-%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: '50m',
|
||||
maxFiles: '90d', // Keep audit logs longer
|
||||
format: combine(
|
||||
timestamp(),
|
||||
json()
|
||||
)
|
||||
});
|
||||
|
||||
// Create the logger
|
||||
export const logger = winston.createLogger({
|
||||
level: config.logging.level || 'info',
|
||||
format: combine(
|
||||
timestamp(),
|
||||
errors({ stack: true }),
|
||||
json()
|
||||
),
|
||||
defaultMeta: { service: 'compliance-guard' },
|
||||
transports: [
|
||||
fileRotateTransport,
|
||||
errorFileTransport
|
||||
]
|
||||
});
|
||||
|
||||
// Create audit logger
|
||||
export const auditLogger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: combine(
|
||||
timestamp(),
|
||||
json()
|
||||
),
|
||||
defaultMeta: { service: 'compliance-guard-audit' },
|
||||
transports: [
|
||||
auditFileTransport
|
||||
]
|
||||
});
|
||||
|
||||
// Add console transport in non-production environments
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: combine(
|
||||
colorize(),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
consoleFormat
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
// Create a stream object for Morgan HTTP logger
|
||||
logger.stream = {
|
||||
write: (message) => {
|
||||
logger.info(message.trim());
|
||||
}
|
||||
};
|
||||
|
||||
// Helper functions for structured logging
|
||||
export const logError = (message, error, metadata = {}) => {
|
||||
logger.error(message, {
|
||||
error: {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
code: error.code
|
||||
},
|
||||
...metadata
|
||||
});
|
||||
};
|
||||
|
||||
export const logWarning = (message, metadata = {}) => {
|
||||
logger.warn(message, metadata);
|
||||
};
|
||||
|
||||
export const logInfo = (message, metadata = {}) => {
|
||||
logger.info(message, metadata);
|
||||
};
|
||||
|
||||
export const logDebug = (message, metadata = {}) => {
|
||||
logger.debug(message, metadata);
|
||||
};
|
||||
|
||||
// Compliance-specific logging functions
|
||||
export const logConsentAction = (action, consentData) => {
|
||||
auditLogger.info('Consent action', {
|
||||
action,
|
||||
consentId: consentData.consentId,
|
||||
userId: consentData.userId,
|
||||
type: consentData.type,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
export const logDataRequest = (action, requestData) => {
|
||||
auditLogger.info('Data request action', {
|
||||
action,
|
||||
requestId: requestData.requestId,
|
||||
userId: requestData.userId,
|
||||
type: requestData.type,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
export const logViolation = (violationData) => {
|
||||
auditLogger.error('Compliance violation', {
|
||||
violationId: violationData.violationId,
|
||||
type: violationData.type,
|
||||
severity: violationData.severity,
|
||||
details: violationData.details,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
export const logSecurityEvent = (event, metadata = {}) => {
|
||||
auditLogger.warn('Security event', {
|
||||
event,
|
||||
...metadata,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
319
marketing-agent/services/compliance-guard/src/utils/metrics.js
Normal file
319
marketing-agent/services/compliance-guard/src/utils/metrics.js
Normal file
@@ -0,0 +1,319 @@
|
||||
import promClient from 'prom-client';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
// Create a Registry
|
||||
const register = new promClient.Registry();
|
||||
|
||||
// Add default metrics
|
||||
promClient.collectDefaultMetrics({ register });
|
||||
|
||||
// Define custom metrics
|
||||
const httpRequestDuration = new promClient.Histogram({
|
||||
name: 'compliance_guard_http_request_duration_seconds',
|
||||
help: 'Duration of HTTP requests in seconds',
|
||||
labelNames: ['method', 'route', 'status_code'],
|
||||
buckets: [0.1, 0.5, 1, 2, 5]
|
||||
});
|
||||
|
||||
const apiCallCounter = new promClient.Counter({
|
||||
name: 'compliance_guard_api_calls_total',
|
||||
help: 'Total number of API calls',
|
||||
labelNames: ['endpoint', 'status']
|
||||
});
|
||||
|
||||
const consentCounter = new promClient.Counter({
|
||||
name: 'compliance_guard_consent_total',
|
||||
help: 'Total number of consent actions',
|
||||
labelNames: ['type', 'action', 'status']
|
||||
});
|
||||
|
||||
const dataRequestCounter = new promClient.Counter({
|
||||
name: 'compliance_guard_data_requests_total',
|
||||
help: 'Total number of data requests',
|
||||
labelNames: ['type', 'status']
|
||||
});
|
||||
|
||||
const violationCounter = new promClient.Counter({
|
||||
name: 'compliance_guard_violations_total',
|
||||
help: 'Total number of compliance violations',
|
||||
labelNames: ['type', 'severity']
|
||||
});
|
||||
|
||||
const activeDataRequestsGauge = new promClient.Gauge({
|
||||
name: 'compliance_guard_active_data_requests',
|
||||
help: 'Number of active data requests',
|
||||
labelNames: ['type']
|
||||
});
|
||||
|
||||
const pendingConsentRenewalsGauge = new promClient.Gauge({
|
||||
name: 'compliance_guard_pending_consent_renewals',
|
||||
help: 'Number of consents pending renewal'
|
||||
});
|
||||
|
||||
const complianceScoreGauge = new promClient.Gauge({
|
||||
name: 'compliance_guard_compliance_score',
|
||||
help: 'Overall compliance score',
|
||||
labelNames: ['regulation']
|
||||
});
|
||||
|
||||
const dataProcessingLatency = new promClient.Histogram({
|
||||
name: 'compliance_guard_data_processing_latency_seconds',
|
||||
help: 'Latency of data processing operations',
|
||||
labelNames: ['operation'],
|
||||
buckets: [0.01, 0.05, 0.1, 0.5, 1, 5, 10]
|
||||
});
|
||||
|
||||
// Register metrics
|
||||
register.registerMetric(httpRequestDuration);
|
||||
register.registerMetric(apiCallCounter);
|
||||
register.registerMetric(consentCounter);
|
||||
register.registerMetric(dataRequestCounter);
|
||||
register.registerMetric(violationCounter);
|
||||
register.registerMetric(activeDataRequestsGauge);
|
||||
register.registerMetric(pendingConsentRenewalsGauge);
|
||||
register.registerMetric(complianceScoreGauge);
|
||||
register.registerMetric(dataProcessingLatency);
|
||||
|
||||
// Metrics recording functions
|
||||
export const metrics = {
|
||||
/**
|
||||
* Record HTTP request duration
|
||||
*/
|
||||
recordHttpRequest: (method, route, statusCode, duration) => {
|
||||
httpRequestDuration
|
||||
.labels(method, route, statusCode.toString())
|
||||
.observe(duration);
|
||||
},
|
||||
|
||||
/**
|
||||
* Record API call
|
||||
*/
|
||||
recordApiCall: (endpoint, status) => {
|
||||
apiCallCounter
|
||||
.labels(endpoint, status)
|
||||
.inc();
|
||||
},
|
||||
|
||||
/**
|
||||
* Record consent action
|
||||
*/
|
||||
recordConsentAction: (type, action, status) => {
|
||||
consentCounter
|
||||
.labels(type, action, status)
|
||||
.inc();
|
||||
},
|
||||
|
||||
/**
|
||||
* Record data request
|
||||
*/
|
||||
recordDataRequest: (type, status) => {
|
||||
dataRequestCounter
|
||||
.labels(type, status)
|
||||
.inc();
|
||||
},
|
||||
|
||||
/**
|
||||
* Record violation
|
||||
*/
|
||||
recordViolation: (type, severity) => {
|
||||
violationCounter
|
||||
.labels(type, severity)
|
||||
.inc();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update active data requests gauge
|
||||
*/
|
||||
updateActiveDataRequests: async (getCountsByType) => {
|
||||
try {
|
||||
const counts = await getCountsByType();
|
||||
for (const [type, count] of Object.entries(counts)) {
|
||||
activeDataRequestsGauge
|
||||
.labels(type)
|
||||
.set(count);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to update active data requests metric:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update pending consent renewals
|
||||
*/
|
||||
updatePendingConsentRenewals: async (getCount) => {
|
||||
try {
|
||||
const count = await getCount();
|
||||
pendingConsentRenewalsGauge.set(count);
|
||||
} catch (error) {
|
||||
logger.error('Failed to update pending consent renewals metric:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update compliance score
|
||||
*/
|
||||
updateComplianceScore: (regulation, score) => {
|
||||
complianceScoreGauge
|
||||
.labels(regulation)
|
||||
.set(score);
|
||||
},
|
||||
|
||||
/**
|
||||
* Record data processing latency
|
||||
*/
|
||||
recordDataProcessingLatency: (operation, duration) => {
|
||||
dataProcessingLatency
|
||||
.labels(operation)
|
||||
.observe(duration);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get metrics for Prometheus
|
||||
*/
|
||||
getMetrics: async () => {
|
||||
return await register.metrics();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get metrics in JSON format
|
||||
*/
|
||||
getMetricsJSON: async () => {
|
||||
return await register.getMetricsAsJSON();
|
||||
}
|
||||
};
|
||||
|
||||
// Middleware to record HTTP metrics
|
||||
export const metricsMiddleware = (req, res, next) => {
|
||||
const start = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = (Date.now() - start) / 1000;
|
||||
const route = req.route ? req.route.path : 'unknown';
|
||||
|
||||
metrics.recordHttpRequest(
|
||||
req.method,
|
||||
route,
|
||||
res.statusCode,
|
||||
duration
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Custom metrics for compliance monitoring
|
||||
class ComplianceMetrics {
|
||||
constructor() {
|
||||
this.resetCounters();
|
||||
}
|
||||
|
||||
resetCounters() {
|
||||
this.counters = {
|
||||
consentGrants: 0,
|
||||
consentWithdrawals: 0,
|
||||
dataRequests: 0,
|
||||
dataRequestsCompleted: 0,
|
||||
violations: 0,
|
||||
violationsResolved: 0
|
||||
};
|
||||
}
|
||||
|
||||
incrementConsentGrants() {
|
||||
this.counters.consentGrants++;
|
||||
}
|
||||
|
||||
incrementConsentWithdrawals() {
|
||||
this.counters.consentWithdrawals++;
|
||||
}
|
||||
|
||||
incrementDataRequests() {
|
||||
this.counters.dataRequests++;
|
||||
}
|
||||
|
||||
incrementDataRequestsCompleted() {
|
||||
this.counters.dataRequestsCompleted++;
|
||||
}
|
||||
|
||||
incrementViolations() {
|
||||
this.counters.violations++;
|
||||
}
|
||||
|
||||
incrementViolationsResolved() {
|
||||
this.counters.violationsResolved++;
|
||||
}
|
||||
|
||||
getComplianceHealth() {
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
metrics: {
|
||||
consentCompletionRate: this.counters.consentWithdrawals > 0
|
||||
? (this.counters.consentGrants / (this.counters.consentGrants + this.counters.consentWithdrawals))
|
||||
: 1,
|
||||
dataRequestCompletionRate: this.counters.dataRequests > 0
|
||||
? (this.counters.dataRequestsCompleted / this.counters.dataRequests)
|
||||
: 1,
|
||||
violationResolutionRate: this.counters.violations > 0
|
||||
? (this.counters.violationsResolved / this.counters.violations)
|
||||
: 1
|
||||
}
|
||||
};
|
||||
|
||||
// Determine overall health status
|
||||
const rates = Object.values(health.metrics);
|
||||
const avgRate = rates.reduce((a, b) => a + b, 0) / rates.length;
|
||||
|
||||
if (avgRate >= 0.95) {
|
||||
health.status = 'healthy';
|
||||
} else if (avgRate >= 0.80) {
|
||||
health.status = 'warning';
|
||||
} else {
|
||||
health.status = 'critical';
|
||||
}
|
||||
|
||||
return health;
|
||||
}
|
||||
}
|
||||
|
||||
export const complianceMetrics = new ComplianceMetrics();
|
||||
|
||||
// Periodic metrics update
|
||||
export const startMetricsCollection = (models) => {
|
||||
// Update metrics every minute
|
||||
setInterval(async () => {
|
||||
try {
|
||||
// Update active data requests
|
||||
if (models.DataRequest) {
|
||||
const counts = await models.DataRequest.aggregate([
|
||||
{ $match: { status: { $in: ['pending', 'processing'] } } },
|
||||
{ $group: { _id: '$type', count: { $sum: 1 } } }
|
||||
]);
|
||||
|
||||
const countsByType = counts.reduce((acc, item) => {
|
||||
acc[item._id] = item.count;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
await metrics.updateActiveDataRequests(async () => countsByType);
|
||||
}
|
||||
|
||||
// Update pending consent renewals
|
||||
if (models.ConsentRecord) {
|
||||
await metrics.updatePendingConsentRenewals(async () => {
|
||||
const thirtyDaysFromNow = new Date();
|
||||
thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30);
|
||||
|
||||
return await models.ConsentRecord.countDocuments({
|
||||
status: 'granted',
|
||||
renewable: true,
|
||||
expiryDate: {
|
||||
$gte: new Date(),
|
||||
$lte: thirtyDaysFromNow
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to update metrics:', error);
|
||||
}
|
||||
}, 60000); // Every minute
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
// Consent validation schemas
|
||||
export const consentSchema = Joi.object({
|
||||
type: Joi.string()
|
||||
.valid('marketing', 'analytics', 'data_processing', 'third_party_sharing', 'cookies')
|
||||
.required(),
|
||||
purposes: Joi.array()
|
||||
.items(Joi.string())
|
||||
.min(1)
|
||||
.required(),
|
||||
duration: Joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.max(730) // Max 2 years
|
||||
.default(365),
|
||||
renewable: Joi.boolean()
|
||||
.default(true),
|
||||
location: Joi.object({
|
||||
country: Joi.string().length(2),
|
||||
region: Joi.string(),
|
||||
city: Joi.string()
|
||||
}),
|
||||
preferences: Joi.object({
|
||||
channels: Joi.array().items(
|
||||
Joi.string().valid('email', 'sms', 'push', 'in_app')
|
||||
),
|
||||
frequency: Joi.string().valid('always', 'daily', 'weekly', 'monthly'),
|
||||
topics: Joi.array().items(Joi.string())
|
||||
})
|
||||
});
|
||||
|
||||
// Data request validation schemas
|
||||
export const dataRequestSchema = Joi.object({
|
||||
userId: Joi.string()
|
||||
.required(),
|
||||
type: Joi.string()
|
||||
.valid('access', 'portability', 'deletion', 'rectification', 'restriction', 'objection')
|
||||
.required(),
|
||||
requestDetails: Joi.object({
|
||||
scope: Joi.string()
|
||||
.valid('all', 'specific')
|
||||
.default('all'),
|
||||
dataCategories: Joi.when('scope', {
|
||||
is: 'specific',
|
||||
then: Joi.array().items(Joi.string()).min(1).required(),
|
||||
otherwise: Joi.array().items(Joi.string())
|
||||
}),
|
||||
format: Joi.string()
|
||||
.valid('json', 'csv', 'xml')
|
||||
.default('json'),
|
||||
delivery: Joi.string()
|
||||
.valid('download', 'email')
|
||||
.default('download'),
|
||||
corrections: Joi.when('type', {
|
||||
is: 'rectification',
|
||||
then: Joi.object().required(),
|
||||
otherwise: Joi.object()
|
||||
}),
|
||||
restrictions: Joi.when('type', {
|
||||
is: 'restriction',
|
||||
then: Joi.array().items(Joi.object({
|
||||
purpose: Joi.string().required(),
|
||||
restriction: Joi.string().required()
|
||||
})).required(),
|
||||
otherwise: Joi.array()
|
||||
}),
|
||||
objections: Joi.when('type', {
|
||||
is: 'objection',
|
||||
then: Joi.array().items(Joi.object({
|
||||
purpose: Joi.string().required(),
|
||||
reason: Joi.string().required()
|
||||
})).required(),
|
||||
otherwise: Joi.array()
|
||||
})
|
||||
}).required(),
|
||||
reason: Joi.string()
|
||||
.max(500),
|
||||
regulation: Joi.string()
|
||||
.valid('gdpr', 'ccpa', 'lgpd', 'other')
|
||||
.default('gdpr')
|
||||
});
|
||||
|
||||
// Violation validation schemas
|
||||
export const violationSchema = Joi.object({
|
||||
type: Joi.string()
|
||||
.valid(
|
||||
'unauthorized_access',
|
||||
'data_leak',
|
||||
'consent_expired',
|
||||
'retention_exceeded',
|
||||
'unauthorized_sharing',
|
||||
'security_breach',
|
||||
'non_compliance',
|
||||
'data_access_denied',
|
||||
'notification_failure'
|
||||
)
|
||||
.required(),
|
||||
severity: Joi.string()
|
||||
.valid('low', 'medium', 'high', 'critical')
|
||||
.required(),
|
||||
details: Joi.object({
|
||||
description: Joi.string()
|
||||
.required(),
|
||||
affectedUsers: Joi.number()
|
||||
.integer()
|
||||
.min(0)
|
||||
.required(),
|
||||
affectedData: Joi.array()
|
||||
.items(Joi.string())
|
||||
.required(),
|
||||
regulation: Joi.string()
|
||||
.valid('gdpr', 'ccpa', 'lgpd', 'other')
|
||||
.required(),
|
||||
articles: Joi.array()
|
||||
.items(Joi.string()),
|
||||
detectionMethod: Joi.string()
|
||||
.valid('automated', 'manual', 'user_report', 'audit')
|
||||
.required()
|
||||
}).required(),
|
||||
remediation: Joi.object({
|
||||
status: Joi.string()
|
||||
.valid('pending', 'in_progress', 'completed')
|
||||
.default('pending'),
|
||||
actions: Joi.array().items(Joi.object({
|
||||
action: Joi.string().required(),
|
||||
description: Joi.string(),
|
||||
targetDate: Joi.date(),
|
||||
status: Joi.string().valid('pending', 'completed').default('pending')
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
// Validation middleware
|
||||
export const validateConsent = (req, res, next) => {
|
||||
const { error } = consentSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: error.details.map(d => d.message)
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
export const validateDataRequest = (req, res, next) => {
|
||||
const { error } = dataRequestSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: error.details.map(d => d.message)
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
export const validateViolation = (req, res, next) => {
|
||||
const { error } = violationSchema.validate(req.body);
|
||||
if (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: error.details.map(d => d.message)
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Additional validation helpers
|
||||
export const validateEmail = (email) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
export const validatePhoneNumber = (phone) => {
|
||||
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
|
||||
return phoneRegex.test(phone);
|
||||
};
|
||||
|
||||
export const validateIPAddress = (ip) => {
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
const ipv6Regex = /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i;
|
||||
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||
};
|
||||
|
||||
export const validateDateRange = (startDate, endDate) => {
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
return start <= end && start <= new Date();
|
||||
};
|
||||
|
||||
export const validateRegulation = (regulation, country) => {
|
||||
const regulationMap = {
|
||||
gdpr: ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR',
|
||||
'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK',
|
||||
'SI', 'ES', 'SE', 'GB', 'IS', 'LI', 'NO'],
|
||||
ccpa: ['US-CA'],
|
||||
lgpd: ['BR']
|
||||
};
|
||||
|
||||
if (!regulationMap[regulation]) return false;
|
||||
|
||||
if (regulation === 'ccpa' && country === 'US') {
|
||||
// CCPA only applies to California
|
||||
return true;
|
||||
}
|
||||
|
||||
return regulationMap[regulation].includes(country);
|
||||
};
|
||||
|
||||
// Sanitization helpers
|
||||
export const sanitizeInput = (input) => {
|
||||
if (typeof input !== 'string') return input;
|
||||
|
||||
// Remove potential XSS vectors
|
||||
return input
|
||||
.replace(/[<>]/g, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/on\w+=/gi, '')
|
||||
.trim();
|
||||
};
|
||||
|
||||
export const sanitizeObject = (obj) => {
|
||||
const sanitized = {};
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (typeof value === 'string') {
|
||||
sanitized[key] = sanitizeInput(value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
sanitized[key] = Array.isArray(value)
|
||||
? value.map(item => typeof item === 'string' ? sanitizeInput(item) : item)
|
||||
: sanitizeObject(value);
|
||||
} else {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
Reference in New Issue
Block a user