Apply Composer changes: comprehensive API updates, migrations, middleware, and infrastructure improvements

- Add comprehensive database migrations (001-024) for schema evolution
- Enhance API schema with expanded type definitions and resolvers
- Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth
- Implement new services: AI optimization, billing, blockchain, compliance, marketplace
- Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage)
- Update Crossplane provider with enhanced VM management capabilities
- Add comprehensive test suite for API endpoints and services
- Update frontend components with improved GraphQL subscriptions and real-time updates
- Enhance security configurations and headers (CSP, CORS, etc.)
- Update documentation and configuration files
- Add new CI/CD workflows and validation scripts
- Implement design system improvements and UI enhancements
This commit is contained in:
defiQUG
2025-12-12 18:01:35 -08:00
parent e01131efaf
commit 9daf1fd378
968 changed files with 160890 additions and 1092 deletions

View File

@@ -0,0 +1,70 @@
/**
* Rate Limiting Middleware Tests
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { rateLimitMiddleware } from '../rate-limit'
import type { FastifyRequest, FastifyReply } from 'fastify'
describe('Rate Limiting Middleware', () => {
let mockRequest: Partial<FastifyRequest>
let mockReply: Partial<FastifyReply>
beforeEach(() => {
mockRequest = {
ip: '127.0.0.1',
socket: {
remoteAddress: '127.0.0.1',
},
}
mockReply = {
code: vi.fn().mockReturnThis(),
send: vi.fn().mockReturnThis(),
header: vi.fn().mockReturnThis(),
}
// Clear module cache to reset store
vi.resetModules()
})
it('should allow requests within rate limit', async () => {
for (let i = 0; i < 50; i++) {
await rateLimitMiddleware(
mockRequest as FastifyRequest,
mockReply as FastifyReply
)
}
expect(mockReply.code).not.toHaveBeenCalledWith(429)
})
it('should reject requests exceeding rate limit', async () => {
// Make 101 requests (exceeding limit of 100)
for (let i = 0; i < 101; i++) {
await rateLimitMiddleware(
mockRequest as FastifyRequest,
mockReply as FastifyReply
)
}
expect(mockReply.code).toHaveBeenCalledWith(429)
expect(mockReply.send).toHaveBeenCalledWith(
expect.objectContaining({
error: 'Too Many Requests',
})
)
})
it('should include rate limit headers', async () => {
await rateLimitMiddleware(
mockRequest as FastifyRequest,
mockReply as FastifyReply
)
expect(mockReply.header).toHaveBeenCalledWith('X-RateLimit-Limit', expect.any(String))
expect(mockReply.header).toHaveBeenCalledWith('X-RateLimit-Remaining', expect.any(String))
expect(mockReply.header).toHaveBeenCalledWith('X-RateLimit-Reset', expect.any(String))
})
})

View File

@@ -0,0 +1,82 @@
/**
* Security Middleware Tests
*/
import { describe, it, expect, vi } from 'vitest'
import {
securityHeadersMiddleware,
sanitizeInput,
sanitizeBodyMiddleware,
} from '../security'
import type { FastifyRequest, FastifyReply } from 'fastify'
describe('Security Middleware', () => {
describe('securityHeadersMiddleware', () => {
it('should add security headers', async () => {
const mockReply = {
header: vi.fn().mockReturnThis(),
} as any
await securityHeadersMiddleware({} as FastifyRequest, mockReply)
expect(mockReply.header).toHaveBeenCalledWith('X-Content-Type-Options', 'nosniff')
expect(mockReply.header).toHaveBeenCalledWith('X-Frame-Options', 'DENY')
expect(mockReply.header).toHaveBeenCalledWith('X-XSS-Protection', '1; mode=block')
expect(mockReply.header).toHaveBeenCalledWith(
'Strict-Transport-Security',
expect.stringContaining('max-age')
)
})
})
describe('sanitizeInput', () => {
it('should remove script tags', () => {
const input = '<script>alert("xss")</script>Hello'
const sanitized = sanitizeInput(input)
expect(sanitized).not.toContain('<script>')
expect(sanitized).toContain('Hello')
})
it('should remove javascript: protocol', () => {
const input = 'javascript:alert("xss")'
const sanitized = sanitizeInput(input)
expect(sanitized).not.toContain('javascript:')
})
it('should sanitize nested objects', () => {
const input = {
name: '<script>alert("xss")</script>',
nested: {
value: 'javascript:test',
},
}
const sanitized = sanitizeInput(input)
expect(sanitized.name).not.toContain('<script>')
expect(sanitized.nested.value).not.toContain('javascript:')
})
it('should sanitize arrays', () => {
const input = ['<script>test</script>', 'normal', 'javascript:test']
const sanitized = sanitizeInput(input)
expect(sanitized[0]).not.toContain('<script>')
expect(sanitized[2]).not.toContain('javascript:')
})
})
describe('sanitizeBodyMiddleware', () => {
it('should sanitize request body', async () => {
const mockRequest = {
body: {
name: '<script>alert("xss")</script>',
},
} as any
const mockReply = {} as FastifyReply
await sanitizeBodyMiddleware(mockRequest, mockReply)
expect(mockRequest.body.name).not.toContain('<script>')
})
})
})

View File

@@ -0,0 +1,71 @@
/**
* Audit Middleware
*
* Automatically logs audit events for all requests
* Per DoD/MilSpec requirements (NIST SP 800-53: AU-2, AU-3)
*/
import { FastifyRequest, FastifyReply } from 'fastify'
import { logAuditEvent, logAuthentication, logDataAccess } from '../services/audit-logger'
import { logger } from '../lib/logger'
/**
* Audit middleware - logs all requests for audit trail
*/
export async function auditMiddleware(
request: FastifyRequest,
reply: FastifyReply
): Promise<void> {
// Skip audit logging for health checks and WebSocket upgrades
if (request.url === '/health' || request.url === '/graphql-ws') {
return
}
const user = (request as any).user
const startTime = Date.now()
// Log request
try {
// Determine event type based on request
if (request.url === '/graphql') {
// GraphQL request - log based on operation
const body = request.body as any
const operation = body?.operationName || 'UNKNOWN'
await logAuditEvent({
eventType: 'DATA_ACCESS',
result: reply.statusCode < 400 ? 'SUCCESS' : 'FAILURE',
userId: user?.id,
userName: user?.name,
userRole: user?.role,
ipAddress: request.ip,
userAgent: request.headers['user-agent'],
action: `GRAPHQL_${operation}`,
details: {
query: body?.query?.substring(0, 200), // Log first 200 chars of query
variables: body?.variables ? 'PRESENT' : 'NONE', // Don't log full variables
},
})
} else {
// Regular HTTP request
await logAuditEvent({
eventType: 'DATA_ACCESS',
result: reply.statusCode < 400 ? 'SUCCESS' : 'FAILURE',
userId: user?.id,
userName: user?.name,
userRole: user?.role,
ipAddress: request.ip,
userAgent: request.headers['user-agent'],
action: `${request.method} ${request.url}`,
details: {
statusCode: reply.statusCode,
responseTime: Date.now() - startTime,
},
})
}
} catch (error) {
// Don't fail the request if audit logging fails, but log the error
logger.error('Failed to log audit event', { error, request: request.url })
}
}

View File

@@ -1,8 +1,12 @@
import { FastifyRequest, FastifyReply } from 'fastify'
import jwt from 'jsonwebtoken'
import { User } from '../types/context'
import { JWTPayload } from '../types/jwt'
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'
const JWT_SECRET = process.env.JWT_SECRET
if (!JWT_SECRET) {
throw new Error('JWT_SECRET environment variable is required')
}
export async function authMiddleware(
request: FastifyRequest,
@@ -23,14 +27,14 @@ export async function authMiddleware(
const token = authHeader.substring(7)
try {
const decoded = jwt.verify(token, JWT_SECRET) as any
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload
// Attach user to request
;(request as any).user = {
;(request as FastifyRequest & { user: User }).user = {
id: decoded.id,
email: decoded.email,
name: decoded.name,
role: decoded.role,
} as User
}
} catch (error) {
// Invalid token - let GraphQL resolvers handle it
return

View File

@@ -0,0 +1,161 @@
/**
* MFA Enforcement Middleware
*
* Enforces MFA requirements per DoD/MilSpec standards:
* - NIST SP 800-53: IA-2 (Identification and Authentication)
* - NIST SP 800-63B: Digital Identity Guidelines
*
* Requires MFA for:
* - All privileged operations
* - Access to classified data
* - Administrative actions
* - Security-sensitive operations
*/
import { FastifyRequest, FastifyReply } from 'fastify'
import { hasMFAEnabled, verifyMFAChallenge } from '../services/mfa'
import { getDb } from '../db'
import { logger } from '../lib/logger'
/**
* Operations that require MFA
*/
const MFA_REQUIRED_OPERATIONS = [
// Administrative operations
'createTenant',
'updateTenant',
'deleteTenant',
'suspendTenant',
'activateTenant',
// Security operations
'createUser',
'updateUser',
'deleteUser',
'changePassword',
'updateRole',
'grantPermission',
'revokePermission',
// Resource management
'createResource',
'updateResource',
'deleteResource',
'provisionVM',
'destroyVM',
// Billing operations
'createInvoice',
'updateBillingAccount',
'processPayment',
// Compliance operations
'exportAuditLog',
'generateComplianceReport',
'updateSecurityPolicy',
]
/**
* Check if an operation requires MFA
*/
function requiresMFA(operation: string): boolean {
return MFA_REQUIRED_OPERATIONS.includes(operation)
}
/**
* Get operation name from GraphQL request
*/
function getOperationName(request: FastifyRequest): string | null {
if (request.body && typeof request.body === 'object') {
const body = request.body as any
if (body.operationName) {
return body.operationName
}
if (body.query) {
// Parse GraphQL query to extract operation name
const match = body.query.match(/(?:mutation|query)\s+(\w+)/)
if (match) {
return match[1]
}
}
}
return null
}
/**
* MFA Enforcement Middleware
* Checks if MFA is required and verified for the current request
*/
export async function mfaEnforcementMiddleware(
request: FastifyRequest,
reply: FastifyReply
): Promise<void> {
// Skip MFA check for health endpoints and WebSocket upgrades
if (request.url === '/health' || request.url === '/graphql-ws') {
return
}
// Get user from request context (set by auth middleware)
const user = (request as any).user
if (!user) {
// Not authenticated - auth middleware will handle
return
}
// Get operation name
const operation = getOperationName(request)
if (!operation || !requiresMFA(operation)) {
// Operation doesn't require MFA
return
}
// Check if user has MFA enabled
const mfaEnabled = await hasMFAEnabled(user.id)
if (!mfaEnabled) {
logger.warn('MFA required but not enabled', { userId: user.id, operation })
reply.code(403).send({
error: 'MFA_REQUIRED',
message: 'Multi-factor authentication is required for this operation. Please enable MFA in your account settings.',
operation,
})
return
}
// Check for MFA challenge verification
const mfaChallengeId = request.headers['x-mfa-challenge-id'] as string
const mfaToken = request.headers['x-mfa-token'] as string
if (!mfaChallengeId || !mfaToken) {
logger.warn('MFA challenge missing', { userId: user.id, operation })
reply.code(403).send({
error: 'MFA_CHALLENGE_REQUIRED',
message: 'MFA challenge verification required for this operation',
operation,
})
return
}
// Verify MFA challenge
const verified = await verifyMFAChallenge(mfaChallengeId, user.id, mfaToken)
if (!verified) {
logger.warn('MFA challenge verification failed', { userId: user.id, operation, challengeId: mfaChallengeId })
reply.code(403).send({
error: 'MFA_VERIFICATION_FAILED',
message: 'MFA verification failed. Please try again.',
operation,
})
return
}
// MFA verified - allow request to proceed
logger.info('MFA verified for operation', { userId: user.id, operation })
}
/**
* Check if MFA is required for a specific operation
* Can be used in GraphQL resolvers
*/
export function checkMFARequired(operation: string, userId: string): Promise<boolean> {
return requiresMFA(operation) ? hasMFAEnabled(userId) : Promise.resolve(false)
}

View File

@@ -0,0 +1,77 @@
/**
* Rate Limiting Middleware
* Implements rate limiting for API endpoints
*/
import { FastifyRequest, FastifyReply } from 'fastify'
interface RateLimitStore {
[key: string]: {
count: number
resetTime: number
}
}
const store: RateLimitStore = {}
const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute
const RATE_LIMIT_MAX_REQUESTS = 100 // 100 requests per minute
/**
* Get client identifier from request
*/
function getClientId(request: FastifyRequest): string {
// Use IP address or user ID
const ip = request.ip || request.socket.remoteAddress || 'unknown'
const userId = (request as any).user?.id
return userId ? `user:${userId}` : `ip:${ip}`
}
/**
* Rate limiting middleware
*/
export async function rateLimitMiddleware(
request: FastifyRequest,
reply: FastifyReply
): Promise<void> {
const clientId = getClientId(request)
const now = Date.now()
// Clean up expired entries
Object.keys(store).forEach((key) => {
if (store[key].resetTime < now) {
delete store[key]
}
})
// Get or create rate limit entry
let entry = store[clientId]
if (!entry || entry.resetTime < now) {
entry = {
count: 0,
resetTime: now + RATE_LIMIT_WINDOW,
}
store[clientId] = entry
}
// Increment count
entry.count++
// Check if limit exceeded
if (entry.count > RATE_LIMIT_MAX_REQUESTS) {
reply.code(429).send({
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.',
retryAfter: Math.ceil((entry.resetTime - now) / 1000),
})
return
}
// Add rate limit headers
reply.header('X-RateLimit-Limit', RATE_LIMIT_MAX_REQUESTS.toString())
reply.header('X-RateLimit-Remaining', Math.max(0, RATE_LIMIT_MAX_REQUESTS - entry.count).toString())
reply.header('X-RateLimit-Reset', entry.resetTime.toString())
}

View File

@@ -0,0 +1,141 @@
/**
* Security Middleware
* Implements security headers and protections per DoD/MilSpec standards
*
* Complies with:
* - DISA STIG: Web Server Security
* - NIST SP 800-53: SI-4 (Information System Monitoring)
* - NIST SP 800-171: 3.13.1 (Cryptographic Protection in Transit)
*/
import { FastifyRequest, FastifyReply } from 'fastify'
import { randomBytes } from 'crypto'
/**
* Add security headers to responses per DoD/MilSpec requirements
*
* Implements comprehensive security headers as required by:
* - DISA STIG for Web Servers
* - OWASP Secure Headers Project
* - DoD Security Technical Implementation Guides
*/
export async function securityHeadersMiddleware(
request: FastifyRequest,
reply: FastifyReply
): Promise<void> {
// Prevent MIME type sniffing (DISA STIG requirement)
reply.header('X-Content-Type-Options', 'nosniff')
// Prevent clickjacking attacks (DISA STIG requirement)
reply.header('X-Frame-Options', 'DENY')
// Legacy XSS protection (deprecated but still recommended for older browsers)
reply.header('X-XSS-Protection', '1; mode=block')
// HTTP Strict Transport Security (HSTS) with preload
// max-age: 1 year (31536000 seconds)
// includeSubDomains: Apply to all subdomains
// preload: Allow inclusion in HSTS preload lists
const hstsMaxAge = 31536000 // 1 year
reply.header('Strict-Transport-Security', `max-age=${hstsMaxAge}; includeSubDomains; preload`)
// Content Security Policy (CSP) per STIG requirements
// Strict CSP to prevent XSS and injection attacks
// Generate nonce for inline scripts/styles to avoid unsafe-inline
const nonce = randomBytes(16).toString('base64')
// Store nonce in request for use in templates
;(request as any).cspNonce = nonce
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`, // Use nonce instead of unsafe-inline
`style-src 'self' 'nonce-${nonce}'`, // Use nonce instead of unsafe-inline
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
].join('; ')
reply.header('Content-Security-Policy', csp)
// Referrer Policy - control referrer information leakage
reply.header('Referrer-Policy', 'strict-origin-when-cross-origin')
// Permissions Policy (formerly Feature Policy) - disable unnecessary features
reply.header('Permissions-Policy', [
'geolocation=()',
'microphone=()',
'camera=()',
'payment=()',
'usb=()',
'magnetometer=()',
'gyroscope=()',
'accelerometer=()',
].join(', '))
// X-Permitted-Cross-Domain-Policies - restrict cross-domain policies
reply.header('X-Permitted-Cross-Domain-Policies', 'none')
// Expect-CT - Certificate Transparency (deprecated but still used)
// Note: This header is deprecated but may still be required for some compliance
// reply.header('Expect-CT', 'max-age=86400, enforce')
// Cross-Origin-Embedder-Policy - prevent cross-origin data leakage
reply.header('Cross-Origin-Embedder-Policy', 'require-corp')
// Cross-Origin-Opener-Policy - isolate browsing context
reply.header('Cross-Origin-Opener-Policy', 'same-origin')
// Cross-Origin-Resource-Policy - control resource loading
reply.header('Cross-Origin-Resource-Policy', 'same-origin')
// Remove server information disclosure
// Note: Fastify doesn't expose server header by default, but we ensure it's not set
reply.removeHeader('Server')
reply.removeHeader('X-Powered-By')
}
/**
* Input sanitization helper
*/
export function sanitizeInput(input: unknown): unknown {
if (typeof input === 'string') {
// Remove potentially dangerous characters
return input
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=/gi, '')
.trim()
}
if (Array.isArray(input)) {
return input.map(sanitizeInput)
}
if (input && typeof input === 'object' && !Array.isArray(input)) {
const sanitized: Record<string, unknown> = {}
for (const key in input) {
if (Object.prototype.hasOwnProperty.call(input, key)) {
sanitized[key] = sanitizeInput((input as Record<string, unknown>)[key])
}
}
return sanitized
}
return input
}
/**
* Validate and sanitize request body
*/
export async function sanitizeBodyMiddleware(
request: FastifyRequest,
reply: FastifyReply
): Promise<void> {
if (request.body) {
request.body = sanitizeInput(request.body)
}
}

View File

@@ -0,0 +1,237 @@
/**
* Tenant-Aware Authentication Middleware
* Enforces tenant isolation in all queries
* Superior to Azure with more flexible permission model
*/
import { FastifyRequest, FastifyReply } from 'fastify'
import { identityService, TokenValidationResult } from '../services/identity.js'
import { getDb } from '../db/index.js'
import { logger } from '../lib/logger.js'
export interface TenantContext {
tenantId?: string
userId: string
email: string
role: string
tenantRole?: string
permissions: Record<string, any>
isSystemAdmin: boolean
}
declare module 'fastify' {
interface FastifyRequest {
tenantContext?: TenantContext
}
}
/**
* Extract tenant context from request
*/
export async function extractTenantContext(
request: FastifyRequest
): Promise<TenantContext | null> {
// Get token from Authorization header
const authHeader = request.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null
}
const token = authHeader.substring(7)
// Validate token
const validation = await identityService.validateToken(token)
if (!validation.valid || !validation.userId) {
return null
}
// Get user from database
const db = getDb()
const userResult = await db.query('SELECT id, email, role FROM users WHERE id = $1', [
validation.userId,
])
if (userResult.rows.length === 0) {
return null
}
const user = userResult.rows[0]
const isSystemAdmin = user.role === 'ADMIN'
// Get tenant information if tenant ID is present
let tenantRole: string | undefined
let tenantPermissions: Record<string, any> = {}
if (validation.tenantId) {
const tenantUserResult = await db.query(
`SELECT role, permissions FROM tenant_users
WHERE tenant_id = $1 AND user_id = $2`,
[validation.tenantId, validation.userId]
)
if (tenantUserResult.rows.length > 0) {
tenantRole = tenantUserResult.rows[0].role
tenantPermissions = tenantUserResult.rows[0].permissions || {}
}
}
return {
tenantId: validation.tenantId,
userId: validation.userId,
email: validation.email || user.email,
role: user.role,
tenantRole,
permissions: { ...tenantPermissions, ...(validation.permissions || {}) },
isSystemAdmin,
}
}
/**
* Tenant-aware authentication middleware
*/
export async function tenantAuthMiddleware(
request: FastifyRequest,
reply: FastifyReply
): Promise<void> {
// Skip auth for health check and GraphQL introspection
if (request.url === '/health' || request.method === 'OPTIONS') {
return
}
const context = await extractTenantContext(request)
if (!context) {
// Allow unauthenticated requests - GraphQL will handle auth per query/mutation
return
}
// Attach tenant context to request
request.tenantContext = context
// Set tenant context in database session for RLS policies
const db = getDb()
if (context.userId) {
await db.query(`SET LOCAL app.current_user_id = $1`, [context.userId])
}
}
/**
* Require authentication middleware
*/
export function requireAuth(
request: FastifyRequest,
reply: FastifyReply
): TenantContext {
const context = request.tenantContext
if (!context) {
reply.code(401).send({
error: 'Authentication required',
code: 'UNAUTHENTICATED',
})
throw new Error('Authentication required')
}
return context
}
/**
* Require tenant membership middleware
*/
export function requireTenant(
request: FastifyRequest,
reply: FastifyReply
): TenantContext {
const context = requireAuth(request, reply)
if (!context.tenantId && !context.isSystemAdmin) {
reply.code(403).send({
error: 'Tenant membership required',
code: 'TENANT_REQUIRED',
})
throw new Error('Tenant membership required')
}
return context
}
/**
* Require specific tenant role
*/
export function requireTenantRole(
allowedRoles: string[]
) {
return (request: FastifyRequest, reply: FastifyReply): TenantContext => {
const context = requireTenant(request, reply)
if (context.isSystemAdmin) {
return context
}
if (!context.tenantRole || !allowedRoles.includes(context.tenantRole)) {
reply.code(403).send({
error: 'Insufficient permissions',
code: 'FORBIDDEN',
required: allowedRoles,
current: context.tenantRole,
})
throw new Error('Insufficient tenant permissions')
}
return context
}
}
/**
* Require system admin middleware
*/
export function requireSystemAdmin(
request: FastifyRequest,
reply: FastifyReply
): TenantContext {
const context = requireAuth(request, reply)
if (!context.isSystemAdmin) {
reply.code(403).send({
error: 'System administrator access required',
code: 'FORBIDDEN',
})
throw new Error('System administrator access required')
}
return context
}
/**
* Filter resources by tenant automatically
*/
export function filterByTenant(
query: string,
params: any[],
context: TenantContext
): { query: string; params: any[] } {
// If system admin, don't filter
if (context.isSystemAdmin) {
return { query, params }
}
// If no tenant context, filter to show only system resources (tenant_id IS NULL)
if (!context.tenantId) {
const whereClause = query.includes('WHERE') ? 'AND tenant_id IS NULL' : 'WHERE tenant_id IS NULL'
return {
query: `${query} ${whereClause}`,
params,
}
}
// Filter by tenant_id
const whereClause = query.includes('WHERE')
? `AND tenant_id = $${params.length + 1}`
: `WHERE tenant_id = $${params.length + 1}`
return {
query: `${query} ${whereClause}`,
params: [...params, context.tenantId],
}
}