diff --git a/services/01-physics-engine/index.js b/services/01-physics-engine/index.js index 66d4442c..3a65b671 100644 --- a/services/01-physics-engine/index.js +++ b/services/01-physics-engine/index.js @@ -21,6 +21,12 @@ const app = express(); const port = process.env.PORT || 3001; const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret_change_in_production'; +const WEAK_SECRETS = ['dev_secret_change_in_production', 'test_secret', 'dev_secret', 'default_secret', 'secret']; +const isWeakSecret = (secret) => { + if (!secret) return true; + return WEAK_SECRETS.includes(secret.toLowerCase().trim()); +}; + const DATABASE_URL = process.env.DATABASE_URL; const REDIS_URL = process.env.REDIS_URL; const KAFKA_BROKERS = (process.env.KAFKA_BROKERS || 'localhost:9092').split(','); @@ -68,6 +74,12 @@ app.use(express.json()); * Middleware: Verify JWT token (Zero-Trust Security) */ const authenticateToken = (req, res, next) => { + // [Security Hardening] Reject weak secrets in production environment + if (process.env.NODE_ENV === 'production' && isWeakSecret(JWT_SECRET)) { + console.error('[Security] JWT_SECRET is weak, insecure, or default. Blocking authenticated endpoint access in production.'); + return res.status(500).json({ error: 'Internal server configuration error' }); + } + const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; diff --git a/services/01-physics-engine/security.test.js b/services/01-physics-engine/security.test.js new file mode 100644 index 00000000..ed3305d6 --- /dev/null +++ b/services/01-physics-engine/security.test.js @@ -0,0 +1,118 @@ +const request = require('supertest'); +const jwt = require('jsonwebtoken'); + +// Mock pg +const mockClient = { + connect: jest.fn(), + query: jest.fn(), + end: jest.fn(), + on: jest.fn() +}; +jest.mock('pg', () => { + return { Client: jest.fn(() => mockClient) }; +}); + +// Mock kafkajs +const mockProducer = { + connect: jest.fn(), + send: jest.fn(), + disconnect: jest.fn() +}; +const mockConsumer = { + connect: jest.fn(), + subscribe: jest.fn(), + run: jest.fn(), + disconnect: jest.fn() +}; +const mockKafka = { + producer: jest.fn(() => mockProducer), + consumer: jest.fn(() => mockConsumer) +}; +jest.mock('kafkajs', () => { + return { Kafka: jest.fn(() => mockKafka) }; +}); + +// Mock redis +const mockRedisClient = { + connect: jest.fn(), + get: jest.fn(), + set: jest.fn(), + setEx: jest.fn(), + hGetAll: jest.fn(), + scan: jest.fn(() => ({ cursor: '0', keys: [] })), + quit: jest.fn() +}; +jest.mock('redis', () => ({ + createClient: jest.fn(() => mockRedisClient) +})); + +describe('L1 Physics Engine Security Hardening', () => { + let originalEnv; + + beforeAll(() => { + originalEnv = { ...process.env }; + }); + + afterEach(() => { + process.env = { ...originalEnv }; + jest.resetModules(); + }); + + test('GET /health should return 200 and run successfully without authentication', async () => { + const { app } = require('./index'); + const res = await request(app).get('/health'); + expect(res.status).toBe(200); + expect(res.body.service).toBe('physics-engine'); + }); + + test('Authenticated route should fail securely with 500 when NODE_ENV is production and JWT_SECRET is weak', async () => { + process.env.NODE_ENV = 'production'; + process.env.JWT_SECRET = 'dev_secret_change_in_production'; // weak secret + + const { app } = require('./index'); + const token = jwt.sign({ driver_id: 'driver-123', fleet_id: 'fleet-abc' }, 'dev_secret_change_in_production'); + + const res = await request(app) + .get('/data/training/physics') + .set('Authorization', `Bearer ${token}`); + + expect(res.status).toBe(500); + expect(res.body.error).toBe('Internal server configuration error'); + }); + + test('Authenticated route should verify token correctly when NODE_ENV is production and JWT_SECRET is strong', async () => { + process.env.NODE_ENV = 'production'; + const strongSecret = 'super_strong_unpredictable_production_secret_key_12345'; + process.env.JWT_SECRET = strongSecret; + + mockClient.query.mockResolvedValueOnce({ + rows: [{ + session_id: 'session-123', + violation_type: 'EFFICIENCY_ALERT', + expected_value: 0.85, + actual_value: 0.70, + severity: 'WARNING', + metadata: {}, + billing_mode: 'FLEET', + vpp_active: true, + v2g_active: false, + iso_region: 'CAISO', + market_price_at_session: 50.0, + physics_score: 0.80, + is_high_fidelity: true, + created_at: new Date().toISOString() + }] + }); + + const { app } = require('./index'); + const token = jwt.sign({ sub: 'admin' }, strongSecret); // System token (no fleet_id) + + const res = await request(app) + .get('/data/training/physics') + .set('Authorization', `Bearer ${token}`); + + expect(res.status).not.toBe(500); + expect(res.status).toBe(200); + expect(res.body.record_count).toBe(1); + }); +});