tlc-claude-code 1.4.8 → 1.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network Security Policy Module
|
|
3
|
+
*
|
|
4
|
+
* Validates network configuration for container security.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DATABASE_IMAGES = [/postgres/i, /mysql/i, /mariadb/i, /mongo/i, /redis/i, /elasticsearch/i];
|
|
8
|
+
const DATABASE_PORTS = ['5432', '3306', '27017', '6379', '9200', '9300'];
|
|
9
|
+
|
|
10
|
+
function isDatabase(name, service) {
|
|
11
|
+
if (DATABASE_IMAGES.some(p => p.test(service.image || ''))) return true;
|
|
12
|
+
if (/db|database|postgres|mysql|mongo|redis/i.test(name)) return true;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validateNetworkConfig(config) {
|
|
17
|
+
const findings = [];
|
|
18
|
+
const services = config.services || {};
|
|
19
|
+
const networks = config.networks || {};
|
|
20
|
+
|
|
21
|
+
const hasCustomNetworks = Object.keys(networks).length > 0;
|
|
22
|
+
const servicesUsingNetworks = Object.values(services).filter(s => s.networks?.length > 0);
|
|
23
|
+
|
|
24
|
+
if (!hasCustomNetworks || servicesUsingNetworks.length === 0) {
|
|
25
|
+
findings.push({
|
|
26
|
+
rule: 'no-default-bridge',
|
|
27
|
+
severity: 'medium',
|
|
28
|
+
message: 'Services using default bridge network. Define custom networks for isolation.',
|
|
29
|
+
fix: 'Create custom networks and assign services to them.',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check database network isolation
|
|
34
|
+
for (const [name, service] of Object.entries(services)) {
|
|
35
|
+
if (isDatabase(name, service) && service.networks) {
|
|
36
|
+
const hasInternalNetwork = service.networks.some(netName => networks[netName]?.internal === true);
|
|
37
|
+
if (!hasInternalNetwork) {
|
|
38
|
+
findings.push({
|
|
39
|
+
rule: 'database-internal-only',
|
|
40
|
+
severity: 'high',
|
|
41
|
+
service: name,
|
|
42
|
+
message: `Database '${name}' should be on internal network only.`,
|
|
43
|
+
fix: 'Add "internal: true" to database network.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for network segmentation
|
|
50
|
+
const networkUsage = {};
|
|
51
|
+
for (const [name, service] of Object.entries(services)) {
|
|
52
|
+
for (const net of service.networks || []) {
|
|
53
|
+
networkUsage[net] = networkUsage[net] || [];
|
|
54
|
+
networkUsage[net].push(name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sharedNetworks = Object.entries(networkUsage).filter(([, svcs]) => svcs.length > 2);
|
|
59
|
+
if (sharedNetworks.length > 0 && Object.keys(networks).length === 1) {
|
|
60
|
+
findings.push({
|
|
61
|
+
rule: 'recommend-network-segmentation',
|
|
62
|
+
severity: 'low',
|
|
63
|
+
message: 'Multiple services share single network. Consider segmenting by function.',
|
|
64
|
+
fix: 'Create separate networks for frontend, backend, and data tiers.',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { findings, score: Math.max(0, 100 - findings.length * 15) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function analyzeNetworkTopology(config) {
|
|
72
|
+
const services = config.services || {};
|
|
73
|
+
const networks = config.networks || {};
|
|
74
|
+
const topology = { services: {}, networks: {}, externalAccessPoints: [] };
|
|
75
|
+
|
|
76
|
+
// Map services to networks
|
|
77
|
+
for (const [name, service] of Object.entries(services)) {
|
|
78
|
+
topology.services[name] = {
|
|
79
|
+
networks: service.networks || [],
|
|
80
|
+
canReach: [],
|
|
81
|
+
ports: service.ports || [],
|
|
82
|
+
};
|
|
83
|
+
if (service.ports?.length > 0) {
|
|
84
|
+
topology.externalAccessPoints.push(name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Calculate reachability
|
|
89
|
+
for (const [name, data] of Object.entries(topology.services)) {
|
|
90
|
+
for (const [otherName, otherData] of Object.entries(topology.services)) {
|
|
91
|
+
if (name !== otherName) {
|
|
92
|
+
const sharedNetworks = data.networks.filter(n => otherData.networks.includes(n));
|
|
93
|
+
if (sharedNetworks.length > 0) {
|
|
94
|
+
data.canReach.push(otherName);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Network info
|
|
101
|
+
for (const [name, config] of Object.entries(networks)) {
|
|
102
|
+
topology.networks[name] = { internal: config.internal || false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return topology;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function detectExposedPorts(config) {
|
|
109
|
+
const findings = [];
|
|
110
|
+
const services = config.services || {};
|
|
111
|
+
|
|
112
|
+
for (const [name, service] of Object.entries(services)) {
|
|
113
|
+
const ports = service.ports || [];
|
|
114
|
+
|
|
115
|
+
for (const portMapping of ports) {
|
|
116
|
+
const portStr = String(portMapping);
|
|
117
|
+
|
|
118
|
+
// Check for database ports exposed
|
|
119
|
+
if (isDatabase(name, service)) {
|
|
120
|
+
const containerPort = portStr.split(':').pop();
|
|
121
|
+
if (DATABASE_PORTS.includes(containerPort)) {
|
|
122
|
+
findings.push({
|
|
123
|
+
rule: 'database-port-exposed',
|
|
124
|
+
severity: 'high',
|
|
125
|
+
service: name,
|
|
126
|
+
message: `Database port ${containerPort} exposed externally.`,
|
|
127
|
+
fix: 'Use "expose" instead of "ports" for internal-only access.',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for binding to all interfaces
|
|
133
|
+
if (portStr.startsWith('0.0.0.0:')) {
|
|
134
|
+
findings.push({
|
|
135
|
+
rule: 'avoid-bind-all-interfaces',
|
|
136
|
+
severity: 'medium',
|
|
137
|
+
service: name,
|
|
138
|
+
message: `Port binding to 0.0.0.0 (all interfaces).`,
|
|
139
|
+
fix: 'Bind to 127.0.0.1 for local-only access.',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { findings, score: Math.max(0, 100 - findings.length * 20) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createNetworkValidator(options = {}) {
|
|
149
|
+
return {
|
|
150
|
+
validate(config) {
|
|
151
|
+
const networkResult = validateNetworkConfig(config);
|
|
152
|
+
const portsResult = detectExposedPorts(config);
|
|
153
|
+
const topology = analyzeNetworkTopology(config);
|
|
154
|
+
|
|
155
|
+
const findings = [...networkResult.findings, ...portsResult.findings];
|
|
156
|
+
const score = Math.max(0, 100 - findings.length * 10);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
findings,
|
|
160
|
+
score,
|
|
161
|
+
topology: {
|
|
162
|
+
nodes: Object.keys(config.services || {}),
|
|
163
|
+
...topology,
|
|
164
|
+
},
|
|
165
|
+
summary: {
|
|
166
|
+
total: findings.length,
|
|
167
|
+
high: findings.filter(f => f.severity === 'high').length,
|
|
168
|
+
medium: findings.filter(f => f.severity === 'medium').length,
|
|
169
|
+
low: findings.filter(f => f.severity === 'low').length,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network Security Policy Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
validateNetworkConfig,
|
|
7
|
+
analyzeNetworkTopology,
|
|
8
|
+
detectExposedPorts,
|
|
9
|
+
createNetworkValidator,
|
|
10
|
+
} from './network-policy.js';
|
|
11
|
+
|
|
12
|
+
describe('network-policy', () => {
|
|
13
|
+
describe('validateNetworkConfig', () => {
|
|
14
|
+
it('detects default bridge network usage', () => {
|
|
15
|
+
const config = { services: { app: { image: 'node:20' } }, networks: {} };
|
|
16
|
+
const result = validateNetworkConfig(config);
|
|
17
|
+
expect(result.findings.some(f => f.rule === 'no-default-bridge')).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('passes with custom networks', () => {
|
|
21
|
+
const config = {
|
|
22
|
+
services: { app: { image: 'node:20', networks: ['custom'] } },
|
|
23
|
+
networks: { custom: {} },
|
|
24
|
+
};
|
|
25
|
+
const result = validateNetworkConfig(config);
|
|
26
|
+
expect(result.findings.some(f => f.rule === 'no-default-bridge')).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('detects database on external network', () => {
|
|
30
|
+
const config = {
|
|
31
|
+
services: { db: { image: 'postgres:16', networks: ['public'] } },
|
|
32
|
+
networks: { public: {} },
|
|
33
|
+
};
|
|
34
|
+
const result = validateNetworkConfig(config);
|
|
35
|
+
expect(result.findings.some(f => f.rule === 'database-internal-only')).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('passes with database on internal network', () => {
|
|
39
|
+
const config = {
|
|
40
|
+
services: { db: { image: 'postgres:16', networks: ['backend'] } },
|
|
41
|
+
networks: { backend: { internal: true } },
|
|
42
|
+
};
|
|
43
|
+
const result = validateNetworkConfig(config);
|
|
44
|
+
expect(result.findings.some(f => f.rule === 'database-internal-only')).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('warns on service with no network isolation', () => {
|
|
48
|
+
const config = {
|
|
49
|
+
services: {
|
|
50
|
+
app: { image: 'node:20', networks: ['shared'] },
|
|
51
|
+
db: { image: 'postgres:16', networks: ['shared'] },
|
|
52
|
+
cache: { image: 'redis:7', networks: ['shared'] },
|
|
53
|
+
},
|
|
54
|
+
networks: { shared: {} },
|
|
55
|
+
};
|
|
56
|
+
const result = validateNetworkConfig(config);
|
|
57
|
+
expect(result.findings.some(f => f.rule === 'recommend-network-segmentation')).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('analyzeNetworkTopology', () => {
|
|
62
|
+
it('identifies service connectivity', () => {
|
|
63
|
+
const config = {
|
|
64
|
+
services: {
|
|
65
|
+
app: { image: 'node:20', networks: ['frontend', 'backend'] },
|
|
66
|
+
db: { image: 'postgres:16', networks: ['backend'] },
|
|
67
|
+
},
|
|
68
|
+
networks: { frontend: {}, backend: { internal: true } },
|
|
69
|
+
};
|
|
70
|
+
const topology = analyzeNetworkTopology(config);
|
|
71
|
+
expect(topology.services.app.networks).toContain('frontend');
|
|
72
|
+
expect(topology.services.app.canReach).toContain('db');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('identifies isolated services', () => {
|
|
76
|
+
const config = {
|
|
77
|
+
services: {
|
|
78
|
+
app: { image: 'node:20', networks: ['frontend'] },
|
|
79
|
+
db: { image: 'postgres:16', networks: ['backend'] },
|
|
80
|
+
},
|
|
81
|
+
networks: { frontend: {}, backend: { internal: true } },
|
|
82
|
+
};
|
|
83
|
+
const topology = analyzeNetworkTopology(config);
|
|
84
|
+
expect(topology.services.app.canReach).not.toContain('db');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('identifies external access points', () => {
|
|
88
|
+
const config = {
|
|
89
|
+
services: { app: { image: 'node:20', ports: ['3000:3000'], networks: ['public'] } },
|
|
90
|
+
networks: { public: {} },
|
|
91
|
+
};
|
|
92
|
+
const topology = analyzeNetworkTopology(config);
|
|
93
|
+
expect(topology.externalAccessPoints).toContain('app');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('detectExposedPorts', () => {
|
|
98
|
+
it('detects unnecessarily exposed database ports', () => {
|
|
99
|
+
const config = {
|
|
100
|
+
services: { db: { image: 'postgres:16', ports: ['5432:5432'] } },
|
|
101
|
+
networks: {},
|
|
102
|
+
};
|
|
103
|
+
const result = detectExposedPorts(config);
|
|
104
|
+
expect(result.findings.some(f => f.rule === 'database-port-exposed')).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('passes with internal-only database', () => {
|
|
108
|
+
const config = {
|
|
109
|
+
services: { db: { image: 'postgres:16', expose: ['5432'] } },
|
|
110
|
+
networks: {},
|
|
111
|
+
};
|
|
112
|
+
const result = detectExposedPorts(config);
|
|
113
|
+
expect(result.findings.some(f => f.rule === 'database-port-exposed')).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('warns on binding to 0.0.0.0', () => {
|
|
117
|
+
const config = {
|
|
118
|
+
services: { app: { image: 'node:20', ports: ['0.0.0.0:3000:3000'] } },
|
|
119
|
+
networks: {},
|
|
120
|
+
};
|
|
121
|
+
const result = detectExposedPorts(config);
|
|
122
|
+
expect(result.findings.some(f => f.rule === 'avoid-bind-all-interfaces')).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('passes with localhost binding', () => {
|
|
126
|
+
const config = {
|
|
127
|
+
services: { app: { image: 'node:20', ports: ['127.0.0.1:3000:3000'] } },
|
|
128
|
+
networks: {},
|
|
129
|
+
};
|
|
130
|
+
const result = detectExposedPorts(config);
|
|
131
|
+
expect(result.findings.some(f => f.rule === 'avoid-bind-all-interfaces')).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('createNetworkValidator', () => {
|
|
136
|
+
it('calculates network security score', () => {
|
|
137
|
+
const validator = createNetworkValidator();
|
|
138
|
+
const secureConfig = {
|
|
139
|
+
services: {
|
|
140
|
+
app: { image: 'node:20', networks: ['frontend', 'backend'] },
|
|
141
|
+
db: { image: 'postgres:16', networks: ['backend'] },
|
|
142
|
+
},
|
|
143
|
+
networks: { frontend: {}, backend: { internal: true } },
|
|
144
|
+
};
|
|
145
|
+
const result = validator.validate(secureConfig);
|
|
146
|
+
expect(result.score).toBeGreaterThanOrEqual(80);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('generates network topology diagram data', () => {
|
|
150
|
+
const validator = createNetworkValidator();
|
|
151
|
+
const config = {
|
|
152
|
+
services: {
|
|
153
|
+
app: { image: 'node:20', networks: ['web'] },
|
|
154
|
+
api: { image: 'node:20', networks: ['web', 'data'] },
|
|
155
|
+
db: { image: 'postgres:16', networks: ['data'] },
|
|
156
|
+
},
|
|
157
|
+
networks: { web: {}, data: { internal: true } },
|
|
158
|
+
};
|
|
159
|
+
const result = validator.validate(config);
|
|
160
|
+
expect(result.topology).toBeDefined();
|
|
161
|
+
expect(result.topology.nodes.length).toBe(3);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Encoder Module
|
|
3
|
+
*
|
|
4
|
+
* Context-aware output encoding to prevent XSS.
|
|
5
|
+
* Addresses OWASP A03: Cross-Site Scripting (XSS)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* HTML entity map
|
|
10
|
+
*/
|
|
11
|
+
const HTML_ENTITIES = {
|
|
12
|
+
'&': '&',
|
|
13
|
+
'<': '<',
|
|
14
|
+
'>': '>',
|
|
15
|
+
'"': '"',
|
|
16
|
+
"'": ''',
|
|
17
|
+
'/': '/',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Additional attribute encoding entities
|
|
22
|
+
*/
|
|
23
|
+
const ATTRIBUTE_ENTITIES = {
|
|
24
|
+
...HTML_ENTITIES,
|
|
25
|
+
'=': '=',
|
|
26
|
+
'`': '`',
|
|
27
|
+
'(': '(',
|
|
28
|
+
')': ')',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Encode a string for HTML content context
|
|
33
|
+
* @param {any} input - The input to encode
|
|
34
|
+
* @param {Object} options - Encoding options
|
|
35
|
+
* @returns {string} Encoded string
|
|
36
|
+
*/
|
|
37
|
+
export function encodeHtml(input, options = {}) {
|
|
38
|
+
const { skipEncoded = false } = options;
|
|
39
|
+
|
|
40
|
+
// Handle null/undefined
|
|
41
|
+
if (input === null || input === undefined) {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Convert to string
|
|
46
|
+
let str = String(input);
|
|
47
|
+
|
|
48
|
+
// Strip null bytes
|
|
49
|
+
str = str.replace(/\x00/g, '');
|
|
50
|
+
|
|
51
|
+
// Skip if already encoded and option is set
|
|
52
|
+
if (skipEncoded && /&[a-z]+;|&#x?[0-9a-f]+;/i.test(str)) {
|
|
53
|
+
return str;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Encode HTML entities
|
|
57
|
+
return str.replace(/[&<>"'\/]/g, (char) => HTML_ENTITIES[char]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Encode a string for HTML attribute context
|
|
62
|
+
* @param {string} input - The input to encode
|
|
63
|
+
* @param {Object} options - Encoding options
|
|
64
|
+
* @returns {string} Encoded string
|
|
65
|
+
*/
|
|
66
|
+
export function encodeHtmlAttribute(input, options = {}) {
|
|
67
|
+
const { context = 'default' } = options;
|
|
68
|
+
|
|
69
|
+
if (input === null || input === undefined) {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let str = String(input);
|
|
74
|
+
|
|
75
|
+
// Strip null bytes
|
|
76
|
+
str = str.replace(/\x00/g, '');
|
|
77
|
+
|
|
78
|
+
// For event handler context, be more restrictive
|
|
79
|
+
if (context === 'event') {
|
|
80
|
+
str = str.replace(/[&<>"'\/=`()]/g, (char) => ATTRIBUTE_ENTITIES[char] || '');
|
|
81
|
+
} else {
|
|
82
|
+
str = str.replace(/[&<>"'\/=`]/g, (char) => ATTRIBUTE_ENTITIES[char]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return str;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Encode a string for JavaScript string context
|
|
90
|
+
* @param {string} input - The input to encode
|
|
91
|
+
* @returns {string} Encoded string
|
|
92
|
+
*/
|
|
93
|
+
export function encodeJavaScript(input) {
|
|
94
|
+
if (input === null || input === undefined) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let str = String(input);
|
|
99
|
+
|
|
100
|
+
// Strip null bytes
|
|
101
|
+
str = str.replace(/\x00/g, '');
|
|
102
|
+
|
|
103
|
+
// Escape backslashes first
|
|
104
|
+
str = str.replace(/\\/g, '\\\\');
|
|
105
|
+
|
|
106
|
+
// Escape quotes
|
|
107
|
+
str = str.replace(/'/g, "\\'");
|
|
108
|
+
str = str.replace(/"/g, '\\"');
|
|
109
|
+
|
|
110
|
+
// Escape newlines
|
|
111
|
+
str = str.replace(/\n/g, '\\n');
|
|
112
|
+
str = str.replace(/\r/g, '\\r');
|
|
113
|
+
|
|
114
|
+
// Escape forward slashes (prevents </script> breaking out)
|
|
115
|
+
str = str.replace(/\//g, '\\/');
|
|
116
|
+
|
|
117
|
+
// Escape unicode line/paragraph separators
|
|
118
|
+
str = str.replace(/\u2028/g, '\\u2028');
|
|
119
|
+
str = str.replace(/\u2029/g, '\\u2029');
|
|
120
|
+
|
|
121
|
+
return str;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Encode a string for URL parameter context
|
|
126
|
+
* @param {string} input - The input to encode
|
|
127
|
+
* @param {Object} options - Encoding options
|
|
128
|
+
* @returns {string} Encoded string
|
|
129
|
+
*/
|
|
130
|
+
export function encodeUrl(input, options = {}) {
|
|
131
|
+
const { preservePath = false } = options;
|
|
132
|
+
|
|
133
|
+
if (input === null || input === undefined) {
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const str = String(input);
|
|
138
|
+
|
|
139
|
+
if (preservePath) {
|
|
140
|
+
// Encode but preserve path separators
|
|
141
|
+
return str.split('/').map((segment) => encodeURIComponent(segment)).join('/');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return encodeURIComponent(str);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Encode a string for CSS context
|
|
149
|
+
* @param {string} input - The input to encode
|
|
150
|
+
* @returns {string} Encoded string
|
|
151
|
+
*/
|
|
152
|
+
export function encodeCss(input) {
|
|
153
|
+
if (input === null || input === undefined) {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let str = String(input);
|
|
158
|
+
|
|
159
|
+
// Strip null bytes
|
|
160
|
+
str = str.replace(/\x00/g, '');
|
|
161
|
+
|
|
162
|
+
// Block javascript: and expression()
|
|
163
|
+
if (/javascript\s*:/i.test(str) || /expression\s*\(/i.test(str)) {
|
|
164
|
+
str = str.replace(/javascript\s*:/gi, '');
|
|
165
|
+
str = str.replace(/expression\s*\(/gi, '');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Block url() with dangerous protocols
|
|
169
|
+
str = str.replace(/url\s*\(\s*["']?\s*javascript:/gi, 'url(');
|
|
170
|
+
str = str.replace(/url\s*\(\s*["']?\s*data:/gi, 'url(');
|
|
171
|
+
|
|
172
|
+
// Escape CSS special characters
|
|
173
|
+
str = str.replace(/\\/g, '\\\\');
|
|
174
|
+
str = str.replace(/"/g, '\\"');
|
|
175
|
+
str = str.replace(/'/g, "\\'");
|
|
176
|
+
str = str.replace(/;/g, '\\;');
|
|
177
|
+
str = str.replace(/\{/g, '\\{');
|
|
178
|
+
str = str.replace(/\}/g, '\\}');
|
|
179
|
+
|
|
180
|
+
return str;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Encode for a specific context
|
|
185
|
+
* @param {string} input - The input to encode
|
|
186
|
+
* @param {string} context - The context (html, javascript, url, css, attribute)
|
|
187
|
+
* @returns {string} Encoded string
|
|
188
|
+
*/
|
|
189
|
+
export function encodeForContext(input, context) {
|
|
190
|
+
switch (context.toLowerCase()) {
|
|
191
|
+
case 'html':
|
|
192
|
+
return encodeHtml(input);
|
|
193
|
+
case 'javascript':
|
|
194
|
+
case 'js':
|
|
195
|
+
return encodeJavaScript(input);
|
|
196
|
+
case 'url':
|
|
197
|
+
return encodeUrl(input);
|
|
198
|
+
case 'css':
|
|
199
|
+
return encodeCss(input);
|
|
200
|
+
case 'attribute':
|
|
201
|
+
case 'attr':
|
|
202
|
+
return encodeHtmlAttribute(input);
|
|
203
|
+
default:
|
|
204
|
+
throw new Error(`Unknown encoding context: ${context}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a chainable encoder
|
|
210
|
+
* @param {Object} options - Encoder options
|
|
211
|
+
* @returns {Object} Encoder instance
|
|
212
|
+
*/
|
|
213
|
+
export function createEncoder(options = {}) {
|
|
214
|
+
const { defaultContext = 'html' } = options;
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
_value: null,
|
|
218
|
+
_context: defaultContext,
|
|
219
|
+
|
|
220
|
+
encode(input, context) {
|
|
221
|
+
const ctx = context || this._context;
|
|
222
|
+
this._value = encodeForContext(input, ctx);
|
|
223
|
+
return this;
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
then(fn) {
|
|
227
|
+
if (this._value !== null) {
|
|
228
|
+
this._value = fn(this._value);
|
|
229
|
+
}
|
|
230
|
+
return this;
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
value() {
|
|
234
|
+
return this._value;
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|