Enhance security middleware and package dependencies

- Updated security headers configuration with stricter settings
- Modified rate limiting and helmet middleware setup
- Added TypeScript type definitions for Express, rate-limit, and Helmet
- Refined referrer policy and HSTS configuration
- Improved security middleware chain for better protection
This commit is contained in:
jango-blockchained
2025-01-30 09:27:22 +01:00
parent 110f2a308c
commit 585b8d1f91
8 changed files with 1277 additions and 150 deletions

View File

@@ -0,0 +1,272 @@
import { jest } from '@jest/globals';
import { ContextManager, ResourceType, RelationType, ResourceState, ResourceRelationship } from '../../src/context/index.js';
describe('Context Manager', () => {
let contextManager: ContextManager;
beforeEach(() => {
contextManager = new ContextManager();
});
describe('Resource Management', () => {
const testResource: ResourceState = {
id: 'test1',
type: ResourceType.DEVICE,
state: 'on',
attributes: { name: 'Test Device' },
lastUpdated: Date.now()
};
it('should add resources', () => {
const addHandler = jest.fn();
contextManager.on('resource_added', addHandler);
contextManager.addResource(testResource);
const resource = contextManager.getResource('test1');
expect(resource).toEqual(testResource);
expect(addHandler).toHaveBeenCalledWith(testResource);
});
it('should update resources', () => {
const updateHandler = jest.fn();
contextManager.on('resource_updated', updateHandler);
contextManager.addResource(testResource);
contextManager.updateResource('test1', {
state: 'off',
attributes: { ...testResource.attributes, brightness: 50 }
});
const resource = contextManager.getResource('test1');
expect(resource?.state).toBe('off');
expect(resource?.attributes.brightness).toBe(50);
expect(updateHandler).toHaveBeenCalled();
});
it('should remove resources', () => {
const removeHandler = jest.fn();
contextManager.on('resource_removed', removeHandler);
contextManager.addResource(testResource);
contextManager.removeResource('test1');
expect(contextManager.getResource('test1')).toBeUndefined();
expect(removeHandler).toHaveBeenCalledWith(testResource);
});
it('should get resources by type', () => {
const resources = [
testResource,
{
id: 'test2',
type: ResourceType.DEVICE,
state: 'off',
attributes: {},
lastUpdated: Date.now()
},
{
id: 'area1',
type: ResourceType.AREA,
state: 'active',
attributes: {},
lastUpdated: Date.now()
}
];
resources.forEach(r => contextManager.addResource(r));
const devices = contextManager.getResourcesByType(ResourceType.DEVICE);
expect(devices).toHaveLength(2);
expect(devices.every((d: ResourceState) => d.type === ResourceType.DEVICE)).toBe(true);
});
});
describe('Relationship Management', () => {
const testRelationship: ResourceRelationship = {
sourceId: 'device1',
targetId: 'area1',
type: RelationType.CONTAINS
};
beforeEach(() => {
// Add test resources
contextManager.addResource({
id: 'device1',
type: ResourceType.DEVICE,
state: 'on',
attributes: {},
lastUpdated: Date.now()
});
contextManager.addResource({
id: 'area1',
type: ResourceType.AREA,
state: 'active',
attributes: {},
lastUpdated: Date.now()
});
});
it('should add relationships', () => {
const addHandler = jest.fn();
contextManager.on('relationship_added', addHandler);
contextManager.addRelationship(testRelationship);
const related = contextManager.getRelatedResources('device1');
expect(related).toHaveLength(2);
expect(related.some(r => r.id === 'area1')).toBe(true);
expect(addHandler).toHaveBeenCalledWith(testRelationship);
});
it('should remove relationships', () => {
const removeHandler = jest.fn();
contextManager.on('relationship_removed', removeHandler);
contextManager.addRelationship(testRelationship);
contextManager.removeRelationship(
'device1',
'area1',
RelationType.CONTAINS
);
const related = contextManager.getRelatedResources('device1');
expect(related).toHaveLength(0);
expect(removeHandler).toHaveBeenCalled();
});
it('should get related resources with depth', () => {
// Create a chain: device1 -> area1 -> area2
contextManager.addResource({
id: 'area2',
type: ResourceType.AREA,
state: 'active',
attributes: {},
lastUpdated: Date.now()
});
contextManager.addRelationship({
sourceId: 'device1',
targetId: 'area1',
type: RelationType.CONTAINS
});
contextManager.addRelationship({
sourceId: 'area1',
targetId: 'area2',
type: RelationType.CONTAINS
});
const relatedDepth1 = contextManager.getRelatedResources('device1', undefined, 1);
expect(relatedDepth1).toHaveLength(3);
const relatedDepth2 = contextManager.getRelatedResources('device1', undefined, 2);
expect(relatedDepth2).toHaveLength(3);
});
});
describe('Resource Analysis', () => {
beforeEach(() => {
// Setup test resources and relationships
const resources = [
{
id: 'device1',
type: ResourceType.DEVICE,
state: 'on',
attributes: {},
lastUpdated: Date.now()
},
{
id: 'automation1',
type: ResourceType.AUTOMATION,
state: 'on',
attributes: {},
lastUpdated: Date.now()
},
{
id: 'group1',
type: ResourceType.GROUP,
state: 'on',
attributes: {},
lastUpdated: Date.now()
}
];
resources.forEach(r => contextManager.addResource(r));
const relationships = [
{
sourceId: 'automation1',
targetId: 'device1',
type: RelationType.CONTROLS
},
{
sourceId: 'device1',
targetId: 'group1',
type: RelationType.DEPENDS_ON
},
{
sourceId: 'group1',
targetId: 'device1',
type: RelationType.GROUPS
}
];
relationships.forEach(r => contextManager.addRelationship(r));
});
it('should analyze resource usage', () => {
const analysis = contextManager.analyzeResourceUsage('device1');
expect(analysis.dependencies).toHaveLength(1);
expect(analysis.dependencies[0]).toBe('group1');
expect(analysis.dependents).toHaveLength(0);
expect(analysis.groups).toHaveLength(1);
expect(analysis.usage.controlCount).toBe(0);
expect(analysis.usage.triggerCount).toBe(0);
expect(analysis.usage.groupCount).toBe(1);
});
});
describe('Event Subscriptions', () => {
it('should handle resource subscriptions', () => {
const callback = jest.fn();
const unsubscribe = contextManager.subscribeToResource('test1', callback);
contextManager.addResource({
id: 'test1',
type: ResourceType.DEVICE,
state: 'on',
attributes: {},
lastUpdated: Date.now()
});
contextManager.updateResource('test1', { state: 'off' });
expect(callback).toHaveBeenCalled();
unsubscribe();
contextManager.updateResource('test1', { state: 'on' });
expect(callback).toHaveBeenCalledTimes(1);
});
it('should handle type subscriptions', () => {
const callback = jest.fn();
const unsubscribe = contextManager.subscribeToType(ResourceType.DEVICE, callback);
const resource = {
id: 'test1',
type: ResourceType.DEVICE,
state: 'on',
attributes: {},
lastUpdated: Date.now()
};
contextManager.addResource(resource);
contextManager.updateResource('test1', { state: 'off' });
expect(callback).toHaveBeenCalled();
unsubscribe();
contextManager.updateResource('test1', { state: 'on' });
expect(callback).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -0,0 +1,189 @@
import { PerformanceMonitor, PerformanceOptimizer, Metric } from '../../src/performance/index.js';
import type { MemoryUsage } from 'node:process';
describe('Performance Module', () => {
describe('PerformanceMonitor', () => {
let monitor: PerformanceMonitor;
beforeEach(() => {
monitor = new PerformanceMonitor({
responseTime: 500,
memoryUsage: 1024 * 1024 * 512, // 512MB
cpuUsage: 70
});
});
afterEach(() => {
monitor.stop();
});
it('should collect metrics', () => {
const metricHandler = jest.fn();
monitor.on('metric', metricHandler);
monitor.start();
// Wait for first collection
return new Promise(resolve => setTimeout(() => {
expect(metricHandler).toHaveBeenCalled();
const calls = metricHandler.mock.calls;
// Verify memory metrics
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'memory.heapUsed'
)).toBe(true);
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'memory.heapTotal'
)).toBe(true);
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'memory.rss'
)).toBe(true);
// Verify CPU metrics
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'cpu.user'
)).toBe(true);
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'cpu.system'
)).toBe(true);
resolve(true);
}, 100));
});
it('should emit threshold exceeded events', () => {
const thresholdHandler = jest.fn();
monitor = new PerformanceMonitor({
memoryUsage: 1, // Ensure threshold is exceeded
cpuUsage: 1
});
monitor.on('threshold_exceeded', thresholdHandler);
monitor.start();
return new Promise(resolve => setTimeout(() => {
expect(thresholdHandler).toHaveBeenCalled();
resolve(true);
}, 100));
});
it('should clean old metrics', () => {
const now = Date.now();
const oldMetric: Metric = {
name: 'test',
value: 1,
timestamp: now - 25 * 60 * 60 * 1000 // 25 hours old
};
const newMetric: Metric = {
name: 'test',
value: 2,
timestamp: now - 1000 // 1 second old
};
monitor.addMetric(oldMetric);
monitor.addMetric(newMetric);
const metrics = monitor.getMetrics(now - 24 * 60 * 60 * 1000);
expect(metrics).toHaveLength(1);
expect(metrics[0]).toEqual(newMetric);
});
it('should calculate metric averages', () => {
const now = Date.now();
const metrics: Metric[] = [
{ name: 'test', value: 1, timestamp: now - 3000 },
{ name: 'test', value: 2, timestamp: now - 2000 },
{ name: 'test', value: 3, timestamp: now - 1000 }
];
metrics.forEach(metric => monitor.addMetric(metric));
const average = monitor.calculateAverage(
'test',
now - 5000,
now
);
expect(average).toBe(2);
});
});
describe('PerformanceOptimizer', () => {
it('should process batches correctly', async () => {
const items = [1, 2, 3, 4, 5];
const batchSize = 2;
const processor = jest.fn(async (batch: number[]) =>
batch.map(n => n * 2)
);
const results = await PerformanceOptimizer.processBatch(
items,
batchSize,
processor
);
expect(results).toEqual([2, 4, 6, 8, 10]);
expect(processor).toHaveBeenCalledTimes(3); // 2 + 2 + 1 items
});
it('should debounce function calls', (done) => {
const fn = jest.fn();
const debounced = PerformanceOptimizer.debounce(fn, 100);
debounced();
debounced();
debounced();
setTimeout(() => {
expect(fn).not.toHaveBeenCalled();
}, 50);
setTimeout(() => {
expect(fn).toHaveBeenCalledTimes(1);
done();
}, 150);
});
it('should throttle function calls', (done) => {
const fn = jest.fn();
const throttled = PerformanceOptimizer.throttle(fn, 100);
throttled();
throttled();
throttled();
expect(fn).toHaveBeenCalledTimes(1);
setTimeout(() => {
throttled();
expect(fn).toHaveBeenCalledTimes(2);
done();
}, 150);
});
it('should optimize memory when threshold is exceeded', async () => {
const originalGc = global.gc;
global.gc = jest.fn();
const memoryUsage = process.memoryUsage;
process.memoryUsage = jest.fn().mockImplementation((): MemoryUsage => ({
heapUsed: 900,
heapTotal: 1000,
rss: 2000,
external: 0,
arrayBuffers: 0
}));
await PerformanceOptimizer.optimizeMemory();
expect(global.gc).toHaveBeenCalled();
// Cleanup
process.memoryUsage = memoryUsage;
if (originalGc) {
global.gc = originalGc;
} else {
delete global.gc;
}
});
});
});

View File

@@ -0,0 +1,212 @@
import { TokenManager, validateRequest, sanitizeInput, errorHandler } from '../../src/security';
import { Request, Response } from 'express';
describe('Security Module', () => {
describe('TokenManager', () => {
const testToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNzE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
const encryptionKey = 'test_encryption_key';
it('should encrypt and decrypt tokens', () => {
const encrypted = TokenManager.encryptToken(testToken, encryptionKey);
const decrypted = TokenManager.decryptToken(encrypted, encryptionKey);
expect(decrypted).toBe(testToken);
});
it('should validate tokens correctly', () => {
expect(TokenManager.validateToken(testToken)).toBe(true);
expect(TokenManager.validateToken('invalid_token')).toBe(false);
expect(TokenManager.validateToken('')).toBe(false);
});
it('should handle expired tokens', () => {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
expect(TokenManager.validateToken(expiredToken)).toBe(false);
});
});
describe('Request Validation', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let mockNext: jest.Mock;
beforeEach(() => {
mockRequest = {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: 'Bearer validToken'
},
is: jest.fn().mockReturnValue(true),
body: { test: 'data' }
};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
mockNext = jest.fn();
});
it('should pass valid requests', () => {
validateRequest(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockNext).toHaveBeenCalled();
expect(mockResponse.status).not.toHaveBeenCalled();
});
it('should reject invalid content type', () => {
mockRequest.is = jest.fn().mockReturnValue(false);
validateRequest(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(415);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Unsupported Media Type - Content-Type must be application/json'
});
});
it('should reject missing token', () => {
mockRequest.headers = {};
validateRequest(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Invalid or expired token'
});
});
it('should reject invalid request body', () => {
mockRequest.body = null;
validateRequest(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Invalid request body'
});
});
});
describe('Input Sanitization', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let mockNext: jest.Mock;
beforeEach(() => {
mockRequest = {
body: {}
};
mockResponse = {};
mockNext = jest.fn();
});
it('should sanitize HTML tags from request body', () => {
mockRequest.body = {
text: 'Test <script>alert("xss")</script>',
nested: {
html: '<img src="x" onerror="alert(1)">'
}
};
sanitizeInput(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockRequest.body).toEqual({
text: 'Test alert("xss")',
nested: {
html: 'img src="x" onerror="alert(1)"'
}
});
expect(mockNext).toHaveBeenCalled();
});
it('should handle non-object body', () => {
mockRequest.body = 'string body';
sanitizeInput(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockRequest.body).toBe('string body');
expect(mockNext).toHaveBeenCalled();
});
});
describe('Error Handler', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let mockNext: jest.Mock;
const originalEnv = process.env.NODE_ENV;
beforeEach(() => {
mockRequest = {};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
mockNext = jest.fn();
});
afterAll(() => {
process.env.NODE_ENV = originalEnv;
});
it('should handle errors in production mode', () => {
process.env.NODE_ENV = 'production';
const error = new Error('Test error');
errorHandler(
error,
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Internal Server Error',
message: undefined
});
});
it('should include error message in development mode', () => {
process.env.NODE_ENV = 'development';
const error = new Error('Test error');
errorHandler(
error,
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Internal Server Error',
message: 'Test error'
});
});
});
});

View File

@@ -0,0 +1 @@

View File

@@ -23,30 +23,30 @@
<div class='clearfix'> <div class='clearfix'>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">38.18% </span> <span class="strong">45.71% </span>
<span class="quiet">Statements</span> <span class="quiet">Statements</span>
<span class='fraction'>42/110</span> <span class='fraction'>128/280</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">12.96% </span> <span class="strong">40.19% </span>
<span class="quiet">Branches</span> <span class="quiet">Branches</span>
<span class='fraction'>7/54</span> <span class='fraction'>41/102</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">28.57% </span> <span class="strong">40.74% </span>
<span class="quiet">Functions</span> <span class="quiet">Functions</span>
<span class='fraction'>2/7</span> <span class='fraction'>33/81</span>
</div> </div>
<div class='fl pad1y space-right2'> <div class='fl pad1y space-right2'>
<span class="strong">38.18% </span> <span class="strong">46.29% </span>
<span class="quiet">Lines</span> <span class="quiet">Lines</span>
<span class='fraction'>42/110</span> <span class='fraction'>125/270</span>
</div> </div>
@@ -79,18 +79,18 @@
</tr> </tr>
</thead> </thead>
<tbody><tr> <tbody><tr>
<td class="file low" data-value="src"><a href="src/index.html">src</a></td> <td class="file high" data-value="src"><a href="src/index.html">src</a></td>
<td data-value="33" class="pic low"> <td data-value="100" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 33%"></div><div class="cover-empty" style="width: 67%"></div></div> <div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
</td> </td>
<td data-value="33" class="pct low">33%</td> <td data-value="100" class="pct high">100%</td>
<td data-value="100" class="abs low">33/100</td> <td data-value="33" class="abs high">33/33</td>
<td data-value="2.43" class="pct low">2.43%</td> <td data-value="100" class="pct high">100%</td>
<td data-value="41" class="abs low">1/41</td> <td data-value="1" class="abs high">1/1</td>
<td data-value="20" class="pct low">20%</td> <td data-value="100" class="pct high">100%</td>
<td data-value="5" class="abs low">1/5</td> <td data-value="1" class="abs high">1/1</td>
<td data-value="33" class="pct low">33%</td> <td data-value="100" class="pct high">100%</td>
<td data-value="100" class="abs low">33/100</td> <td data-value="33" class="abs high">33/33</td>
</tr> </tr>
<tr> <tr>
@@ -108,6 +108,21 @@
<td data-value="2" class="abs high">2/2</td> <td data-value="2" class="abs high">2/2</td>
</tr> </tr>
<tr>
<td class="file high" data-value="src/context"><a href="src/context/index.html">src/context</a></td>
<td data-value="95.55" class="pic high">
<div class="chart"><div class="cover-fill" style="width: 95%"></div><div class="cover-empty" style="width: 5%"></div></div>
</td>
<td data-value="95.55" class="pct high">95.55%</td>
<td data-value="90" class="abs high">86/90</td>
<td data-value="85" class="pct high">85%</td>
<td data-value="40" class="abs high">34/40</td>
<td data-value="91.17" class="pct high">91.17%</td>
<td data-value="34" class="abs high">31/34</td>
<td data-value="95.4" class="pct high">95.4%</td>
<td data-value="87" class="abs high">83/87</td>
</tr>
<tr> <tr>
<td class="file high" data-value="src/hass"><a href="src/hass/index.html">src/hass</a></td> <td class="file high" data-value="src/hass"><a href="src/hass/index.html">src/hass</a></td>
<td data-value="87.5" class="pic high"> <td data-value="87.5" class="pic high">
@@ -123,6 +138,36 @@
<td data-value="8" class="abs high">7/8</td> <td data-value="8" class="abs high">7/8</td>
</tr> </tr>
<tr>
<td class="file low" data-value="src/performance"><a href="src/performance/index.html">src/performance</a></td>
<td data-value="0" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="67" class="abs low">0/67</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="22" class="abs low">0/22</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="21" class="abs low">0/21</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="60" class="abs low">0/60</td>
</tr>
<tr>
<td class="file low" data-value="src/websocket"><a href="src/websocket/index.html">src/websocket</a></td>
<td data-value="0" class="pic low">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="80" class="abs low">0/80</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="26" class="abs low">0/26</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="23" class="abs low">0/23</td>
<td data-value="0" class="pct low">0%</td>
<td data-value="80" class="abs low">0/80</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -131,7 +176,7 @@
<div class='footer quiet pad2 space-top1 center small'> <div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2024-12-21T09:04:36.269Z at 2025-01-30T08:26:17.384Z
</div> </div>
<script src="prettify.js"></script> <script src="prettify.js"></script>
<script> <script>

View File

@@ -13,130 +13,6 @@ BRF:1
BRH:1 BRH:1
end_of_record end_of_record
TN: TN:
SF:src/index.ts
FN:49,main
FN:60,(anonymous_1)
FN:74,(anonymous_2)
FN:136,(anonymous_3)
FNF:4
FNH:0
FNDA:0,main
FNDA:0,(anonymous_1)
FNDA:0,(anonymous_2)
FNDA:0,(anonymous_3)
DA:8,0
DA:9,0
DA:32,0
DA:33,0
DA:34,0
DA:50,0
DA:53,0
DA:56,0
DA:61,0
DA:62,0
DA:69,0
DA:70,0
DA:73,0
DA:74,0
DA:75,0
DA:76,0
DA:77,0
DA:79,0
DA:84,0
DA:87,0
DA:92,0
DA:101,0
DA:137,0
DA:138,0
DA:140,0
DA:141,0
DA:144,0
DA:145,0
DA:150,0
DA:152,0
DA:153,0
DA:155,0
DA:156,0
DA:158,0
DA:159,0
DA:161,0
DA:164,0
DA:165,0
DA:167,0
DA:168,0
DA:170,0
DA:173,0
DA:174,0
DA:175,0
DA:177,0
DA:178,0
DA:180,0
DA:181,0
DA:184,0
DA:185,0
DA:187,0
DA:188,0
DA:190,0
DA:191,0
DA:193,0
DA:198,0
DA:201,0
DA:205,0
DA:206,0
DA:215,0
DA:216,0
DA:219,0
DA:224,0
DA:227,0
DA:236,0
DA:237,0
DA:240,0
LF:67
LH:0
BRDA:8,0,0,0
BRDA:8,0,1,0
BRDA:69,1,0,0
BRDA:76,2,0,0
BRDA:94,3,0,0
BRDA:94,3,1,0
BRDA:140,4,0,0
BRDA:150,5,0,0
BRDA:150,5,1,0
BRDA:150,5,2,0
BRDA:150,5,3,0
BRDA:150,5,4,0
BRDA:150,5,5,0
BRDA:152,6,0,0
BRDA:155,7,0,0
BRDA:158,8,0,0
BRDA:164,9,0,0
BRDA:164,10,0,0
BRDA:164,10,1,0
BRDA:167,11,0,0
BRDA:167,12,0,0
BRDA:167,12,1,0
BRDA:173,13,0,0
BRDA:174,14,0,0
BRDA:177,15,0,0
BRDA:180,16,0,0
BRDA:184,17,0,0
BRDA:184,18,0,0
BRDA:184,18,1,0
BRDA:187,19,0,0
BRDA:187,20,0,0
BRDA:187,20,1,0
BRDA:190,21,0,0
BRDA:190,22,0,0
BRDA:190,22,1,0
BRDA:215,23,0,0
BRDA:224,24,0,0
BRDA:224,24,1,0
BRDA:229,25,0,0
BRDA:229,25,1,0
BRF:40
BRH:0
end_of_record
TN:
SF:src/schemas.ts SF:src/schemas.ts
FNF:0 FNF:0
FNH:0 FNH:0
@@ -196,6 +72,210 @@ BRF:8
BRH:4 BRH:4
end_of_record end_of_record
TN: TN:
SF:src/context/index.ts
FN:4,(anonymous_0)
FN:25,(anonymous_1)
FN:48,(anonymous_2)
FN:53,(anonymous_3)
FN:58,(anonymous_4)
FN:75,(anonymous_5)
FN:81,(anonymous_6)
FN:88,(anonymous_7)
FN:93,(anonymous_8)
FN:95,(anonymous_9)
FN:104,(anonymous_10)
FN:113,(anonymous_11)
FN:118,(anonymous_12)
FN:122,(anonymous_13)
FN:124,(anonymous_14)
FN:128,(anonymous_15)
FN:136,(anonymous_16)
FN:141,(anonymous_17)
FN:145,(anonymous_18)
FN:160,(anonymous_19)
FN:171,(anonymous_20)
FN:172,(anonymous_21)
FN:175,(anonymous_22)
FN:176,(anonymous_23)
FN:179,(anonymous_24)
FN:180,(anonymous_25)
FN:184,(anonymous_26)
FN:187,(anonymous_27)
FN:196,(anonymous_28)
FN:200,(anonymous_29)
FN:207,(anonymous_30)
FN:210,(anonymous_31)
FN:214,(anonymous_32)
FN:221,(anonymous_33)
FNF:34
FNH:31
FNDA:1,(anonymous_0)
FNDA:1,(anonymous_1)
FNDA:11,(anonymous_2)
FNDA:18,(anonymous_3)
FNDA:5,(anonymous_4)
FNDA:1,(anonymous_5)
FNDA:0,(anonymous_6)
FNDA:7,(anonymous_7)
FNDA:1,(anonymous_8)
FNDA:1,(anonymous_9)
FNDA:5,(anonymous_10)
FNDA:0,(anonymous_11)
FNDA:3,(anonymous_12)
FNDA:1,(anonymous_13)
FNDA:3,(anonymous_14)
FNDA:4,(anonymous_15)
FNDA:13,(anonymous_16)
FNDA:12,(anonymous_17)
FNDA:9,(anonymous_18)
FNDA:1,(anonymous_19)
FNDA:3,(anonymous_20)
FNDA:1,(anonymous_21)
FNDA:3,(anonymous_22)
FNDA:0,(anonymous_23)
FNDA:3,(anonymous_24)
FNDA:1,(anonymous_25)
FNDA:3,(anonymous_26)
FNDA:3,(anonymous_27)
FNDA:1,(anonymous_28)
FNDA:1,(anonymous_29)
FNDA:1,(anonymous_30)
FNDA:1,(anonymous_31)
FNDA:1,(anonymous_32)
FNDA:1,(anonymous_33)
DA:4,1
DA:5,1
DA:6,1
DA:7,1
DA:8,1
DA:9,1
DA:10,1
DA:11,1
DA:25,1
DA:26,1
DA:27,1
DA:28,1
DA:29,1
DA:30,1
DA:43,11
DA:44,11
DA:45,11
DA:46,11
DA:49,11
DA:54,18
DA:55,18
DA:59,5
DA:60,5
DA:62,5
DA:65,5
DA:70,5
DA:71,5
DA:76,1
DA:77,1
DA:78,1
DA:80,1
DA:81,0
DA:83,1
DA:89,7
DA:90,7
DA:94,1
DA:95,1
DA:97,1
DA:98,1
DA:99,1
DA:105,5
DA:106,5
DA:107,5
DA:108,0
DA:110,5
DA:114,0
DA:119,3
DA:123,1
DA:124,3
DA:133,4
DA:134,4
DA:136,4
DA:137,13
DA:138,8
DA:140,8
DA:142,12
DA:146,9
DA:147,9
DA:148,9
DA:149,9
DA:150,9
DA:155,4
DA:156,4
DA:170,1
DA:171,3
DA:172,1
DA:174,1
DA:175,3
DA:176,0
DA:178,1
DA:179,3
DA:180,1
DA:182,1
DA:184,3
DA:187,3
DA:192,1
DA:200,1
DA:201,1
DA:202,1
DA:206,1
DA:207,1
DA:214,1
DA:215,1
DA:216,1
DA:220,1
DA:221,1
DA:226,1
LF:87
LH:83
BRDA:4,0,0,1
BRDA:4,0,1,1
BRDA:25,1,0,1
BRDA:25,1,1,1
BRDA:60,2,0,5
BRDA:77,3,0,1
BRDA:81,4,0,0
BRDA:81,4,1,0
BRDA:95,5,0,1
BRDA:95,5,1,1
BRDA:95,5,2,1
BRDA:97,6,0,1
BRDA:105,7,0,5
BRDA:105,7,1,3
BRDA:107,8,0,0
BRDA:114,9,0,0
BRDA:114,9,1,0
BRDA:131,10,0,2
BRDA:137,11,0,5
BRDA:137,12,0,13
BRDA:137,12,1,9
BRDA:142,13,0,12
BRDA:142,13,1,7
BRDA:142,13,2,9
BRDA:142,13,3,0
BRDA:146,14,0,5
BRDA:146,14,1,4
BRDA:148,15,0,9
BRDA:171,16,0,3
BRDA:171,16,1,1
BRDA:175,17,0,3
BRDA:175,17,1,2
BRDA:179,18,0,3
BRDA:179,18,1,2
BRDA:184,19,0,3
BRDA:184,19,1,1
BRDA:187,20,0,3
BRDA:187,20,1,1
BRDA:201,21,0,1
BRDA:215,22,0,1
BRF:40
BRH:34
end_of_record
TN:
SF:src/hass/index.ts SF:src/hass/index.ts
FN:66,(anonymous_0) FN:66,(anonymous_0)
FN:107,get_hass FN:107,get_hass
@@ -221,3 +301,297 @@ BRDA:110,2,1,0
BRF:5 BRF:5
BRH:2 BRH:2
end_of_record end_of_record
TN:
SF:src/performance/index.ts
FN:42,(anonymous_1)
FN:43,(anonymous_2)
FN:50,(anonymous_3)
FN:57,(anonymous_4)
FN:99,(anonymous_5)
FN:105,(anonymous_6)
FN:107,(anonymous_7)
FN:111,(anonymous_8)
FN:134,(anonymous_9)
FN:139,(anonymous_10)
FN:147,(anonymous_11)
FN:154,(anonymous_12)
FN:163,(anonymous_13)
FN:175,(anonymous_14)
FN:185,(anonymous_15)
FN:191,(anonymous_16)
FN:196,(anonymous_17)
FN:198,(anonymous_18)
FN:203,(anonymous_19)
FN:208,(anonymous_20)
FN:212,(anonymous_21)
FNF:21
FNH:0
FNDA:0,(anonymous_1)
FNDA:0,(anonymous_2)
FNDA:0,(anonymous_3)
FNDA:0,(anonymous_4)
FNDA:0,(anonymous_5)
FNDA:0,(anonymous_6)
FNDA:0,(anonymous_7)
FNDA:0,(anonymous_8)
FNDA:0,(anonymous_9)
FNDA:0,(anonymous_10)
FNDA:0,(anonymous_11)
FNDA:0,(anonymous_12)
FNDA:0,(anonymous_13)
FNDA:0,(anonymous_14)
FNDA:0,(anonymous_15)
FNDA:0,(anonymous_16)
FNDA:0,(anonymous_17)
FNDA:0,(anonymous_18)
FNDA:0,(anonymous_19)
FNDA:0,(anonymous_20)
FNDA:0,(anonymous_21)
DA:20,0
DA:31,0
DA:32,0
DA:37,0
DA:38,0
DA:43,0
DA:44,0
DA:45,0
DA:51,0
DA:52,0
DA:58,0
DA:59,0
DA:60,0
DA:63,0
DA:69,0
DA:75,0
DA:82,0
DA:88,0
DA:95,0
DA:100,0
DA:101,0
DA:106,0
DA:107,0
DA:112,0
DA:113,0
DA:114,0
DA:121,0
DA:122,0
DA:123,0
DA:124,0
DA:125,0
DA:139,0
DA:140,0
DA:152,0
DA:153,0
DA:154,0
DA:160,0
DA:164,0
DA:165,0
DA:167,0
DA:168,0
DA:169,0
DA:180,0
DA:181,0
DA:182,0
DA:183,0
DA:184,0
DA:185,0
DA:187,0
DA:196,0
DA:197,0
DA:198,0
DA:207,0
DA:208,0
DA:209,0
DA:210,0
DA:211,0
DA:212,0
DA:219,0
DA:222,0
LF:60
LH:0
BRDA:27,0,0,0
BRDA:28,1,0,0
BRDA:29,2,0,0
BRDA:33,3,0,0
BRDA:33,3,1,0
BRDA:34,4,0,0
BRDA:34,4,1,0
BRDA:35,5,0,0
BRDA:35,5,1,0
BRDA:51,6,0,0
BRDA:113,7,0,0
BRDA:124,8,0,0
BRDA:136,9,0,0
BRDA:140,10,0,0
BRDA:140,10,1,0
BRDA:140,10,2,0
BRDA:140,10,3,0
BRDA:150,11,0,0
BRDA:153,12,0,0
BRDA:167,13,0,0
BRDA:168,14,0,0
BRDA:209,15,0,0
BRF:22
BRH:0
end_of_record
TN:
SF:src/websocket/client.ts
FN:13,(anonymous_0)
FN:27,(anonymous_1)
FN:28,(anonymous_2)
FN:32,(anonymous_3)
FN:36,(anonymous_4)
FN:41,(anonymous_5)
FN:45,(anonymous_6)
FN:50,(anonymous_7)
FN:56,(anonymous_8)
FN:65,(anonymous_9)
FN:72,(anonymous_10)
FN:92,(anonymous_11)
FN:100,(anonymous_12)
FN:105,(anonymous_13)
FN:107,(anonymous_14)
FN:114,(anonymous_15)
FN:122,(anonymous_16)
FN:129,(anonymous_17)
FN:139,(anonymous_18)
FN:145,(anonymous_19)
FN:152,(anonymous_20)
FN:162,(anonymous_21)
FN:168,(anonymous_22)
FNF:23
FNH:0
FNDA:0,(anonymous_0)
FNDA:0,(anonymous_1)
FNDA:0,(anonymous_2)
FNDA:0,(anonymous_3)
FNDA:0,(anonymous_4)
FNDA:0,(anonymous_5)
FNDA:0,(anonymous_6)
FNDA:0,(anonymous_7)
FNDA:0,(anonymous_8)
FNDA:0,(anonymous_9)
FNDA:0,(anonymous_10)
FNDA:0,(anonymous_11)
FNDA:0,(anonymous_12)
FNDA:0,(anonymous_13)
FNDA:0,(anonymous_14)
FNDA:0,(anonymous_15)
FNDA:0,(anonymous_16)
FNDA:0,(anonymous_17)
FNDA:0,(anonymous_18)
FNDA:0,(anonymous_19)
FNDA:0,(anonymous_20)
FNDA:0,(anonymous_21)
FNDA:0,(anonymous_22)
DA:5,0
DA:6,0
DA:7,0
DA:8,0
DA:9,0
DA:10,0
DA:11,0
DA:14,0
DA:15,0
DA:16,0
DA:22,0
DA:23,0
DA:24,0
DA:28,0
DA:29,0
DA:30,0
DA:32,0
DA:33,0
DA:36,0
DA:37,0
DA:38,0
DA:41,0
DA:42,0
DA:45,0
DA:46,0
DA:47,0
DA:50,0
DA:51,0
DA:52,0
DA:53,0
DA:56,0
DA:57,0
DA:60,0
DA:66,0
DA:73,0
DA:75,0
DA:76,0
DA:78,0
DA:79,0
DA:81,0
DA:82,0
DA:84,0
DA:85,0
DA:87,0
DA:88,0
DA:93,0
DA:94,0
DA:95,0
DA:97,0
DA:101,0
DA:102,0
DA:104,0
DA:105,0
DA:106,0
DA:107,0
DA:108,0
DA:115,0
DA:116,0
DA:119,0
DA:120,0
DA:122,0
DA:123,0
DA:129,0
DA:130,0
DA:131,0
DA:133,0
DA:140,0
DA:141,0
DA:144,0
DA:145,0
DA:146,0
DA:152,0
DA:153,0
DA:154,0
DA:156,0
DA:163,0
DA:164,0
DA:169,0
DA:170,0
DA:171,0
LF:80
LH:0
BRDA:16,0,0,0
BRDA:23,1,0,0
BRDA:23,1,1,0
BRDA:24,2,0,0
BRDA:24,2,1,0
BRDA:73,3,0,0
BRDA:73,3,1,0
BRDA:73,3,2,0
BRDA:73,3,3,0
BRDA:73,3,4,0
BRDA:94,4,0,0
BRDA:104,5,0,0
BRDA:104,6,0,0
BRDA:104,6,1,0
BRDA:115,7,0,0
BRDA:130,8,0,0
BRDA:130,8,1,0
BRDA:133,9,0,0
BRDA:133,9,1,0
BRDA:140,10,0,0
BRDA:153,11,0,0
BRDA:153,11,1,0
BRDA:156,12,0,0
BRDA:156,12,1,0
BRDA:163,13,0,0
BRDA:169,14,0,0
BRF:26
BRH:0
end_of_record

View File

@@ -28,8 +28,12 @@
}, },
"devDependencies": { "devDependencies": {
"@types/ajv": "^0.0.5", "@types/ajv": "^0.0.5",
"@types/express": "^5.0.0",
"@types/express-rate-limit": "^5.1.3",
"@types/helmet": "^0.0.48",
"@types/jest": "^28.1.8", "@types/jest": "^28.1.8",
"@types/node": "^20.17.10", "@types/node": "^20.17.10",
"@types/ws": "^8.5.14",
"jest": "^28.1.3", "jest": "^28.1.3",
"semver": "^6.3.1", "semver": "^6.3.1",
"ts-jest": "^28.0.8", "ts-jest": "^28.0.8",

View File

@@ -24,19 +24,25 @@ export const securityHeaders = helmet({
styleSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'], imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", process.env.HASS_HOST || ''], connectSrc: ["'self'", process.env.HASS_HOST || ''],
upgradeInsecureRequests: [] upgradeInsecureRequests: true
} }
}, },
crossOriginEmbedderPolicy: true, crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: true, crossOriginOpenerPolicy: true,
crossOriginResourcePolicy: { policy: 'same-site' }, crossOriginResourcePolicy: { policy: 'same-site' },
dnsPrefetchControl: true, dnsPrefetchControl: true,
frameguard: { action: 'deny' }, frameguard: {
action: 'deny'
},
hidePoweredBy: true, hidePoweredBy: true,
hsts: true, hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
ieNoOpen: true, ieNoOpen: true,
noSniff: true, noSniff: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, referrerPolicy: { policy: 'no-referrer' },
xssFilter: true xssFilter: true
}); });
@@ -172,8 +178,32 @@ export function errorHandler(err: Error, req: Request, res: Response, next: Next
// Export security middleware chain // Export security middleware chain
export const securityMiddleware = [ export const securityMiddleware = [
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", process.env.HASS_HOST || ''],
upgradeInsecureRequests: true
}
},
dnsPrefetchControl: true,
frameguard: {
action: 'deny'
},
hidePoweredBy: true,
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
noSniff: true,
referrerPolicy: { policy: 'no-referrer' },
xssFilter: true
}),
rateLimiter, rateLimiter,
securityHeaders,
validateRequest, validateRequest,
sanitizeInput, sanitizeInput,
errorHandler errorHandler