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.
Files changed (169) hide show
  1. package/package.json +1 -1
  2. package/server/index.js +229 -14
  3. package/server/lib/compliance/control-mapper.js +401 -0
  4. package/server/lib/compliance/control-mapper.test.js +117 -0
  5. package/server/lib/compliance/evidence-linker.js +296 -0
  6. package/server/lib/compliance/evidence-linker.test.js +121 -0
  7. package/server/lib/compliance/gdpr-checklist.js +416 -0
  8. package/server/lib/compliance/gdpr-checklist.test.js +131 -0
  9. package/server/lib/compliance/hipaa-checklist.js +277 -0
  10. package/server/lib/compliance/hipaa-checklist.test.js +101 -0
  11. package/server/lib/compliance/iso27001-checklist.js +287 -0
  12. package/server/lib/compliance/iso27001-checklist.test.js +99 -0
  13. package/server/lib/compliance/multi-framework-reporter.js +284 -0
  14. package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
  15. package/server/lib/compliance/pci-dss-checklist.js +214 -0
  16. package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
  17. package/server/lib/compliance/trust-centre.js +187 -0
  18. package/server/lib/compliance/trust-centre.test.js +93 -0
  19. package/server/lib/dashboard/api-server.js +155 -0
  20. package/server/lib/dashboard/api-server.test.js +155 -0
  21. package/server/lib/dashboard/health-api.js +199 -0
  22. package/server/lib/dashboard/health-api.test.js +122 -0
  23. package/server/lib/dashboard/notes-api.js +234 -0
  24. package/server/lib/dashboard/notes-api.test.js +134 -0
  25. package/server/lib/dashboard/router-api.js +176 -0
  26. package/server/lib/dashboard/router-api.test.js +132 -0
  27. package/server/lib/dashboard/tasks-api.js +289 -0
  28. package/server/lib/dashboard/tasks-api.test.js +161 -0
  29. package/server/lib/dashboard/tlc-introspection.js +197 -0
  30. package/server/lib/dashboard/tlc-introspection.test.js +138 -0
  31. package/server/lib/dashboard/version-api.js +222 -0
  32. package/server/lib/dashboard/version-api.test.js +112 -0
  33. package/server/lib/dashboard/websocket-server.js +104 -0
  34. package/server/lib/dashboard/websocket-server.test.js +118 -0
  35. package/server/lib/deploy/branch-classifier.js +163 -0
  36. package/server/lib/deploy/branch-classifier.test.js +164 -0
  37. package/server/lib/deploy/deployment-approval.js +299 -0
  38. package/server/lib/deploy/deployment-approval.test.js +296 -0
  39. package/server/lib/deploy/deployment-audit.js +374 -0
  40. package/server/lib/deploy/deployment-audit.test.js +307 -0
  41. package/server/lib/deploy/deployment-executor.js +335 -0
  42. package/server/lib/deploy/deployment-executor.test.js +329 -0
  43. package/server/lib/deploy/deployment-rules.js +163 -0
  44. package/server/lib/deploy/deployment-rules.test.js +188 -0
  45. package/server/lib/deploy/rollback-manager.js +379 -0
  46. package/server/lib/deploy/rollback-manager.test.js +321 -0
  47. package/server/lib/deploy/security-gates.js +236 -0
  48. package/server/lib/deploy/security-gates.test.js +222 -0
  49. package/server/lib/k8s/gitops-config.js +188 -0
  50. package/server/lib/k8s/gitops-config.test.js +59 -0
  51. package/server/lib/k8s/helm-generator.js +196 -0
  52. package/server/lib/k8s/helm-generator.test.js +59 -0
  53. package/server/lib/k8s/kustomize-generator.js +176 -0
  54. package/server/lib/k8s/kustomize-generator.test.js +58 -0
  55. package/server/lib/k8s/network-policy.js +114 -0
  56. package/server/lib/k8s/network-policy.test.js +53 -0
  57. package/server/lib/k8s/pod-security.js +114 -0
  58. package/server/lib/k8s/pod-security.test.js +55 -0
  59. package/server/lib/k8s/rbac-generator.js +132 -0
  60. package/server/lib/k8s/rbac-generator.test.js +57 -0
  61. package/server/lib/k8s/resource-manager.js +172 -0
  62. package/server/lib/k8s/resource-manager.test.js +60 -0
  63. package/server/lib/k8s/secrets-encryption.js +168 -0
  64. package/server/lib/k8s/secrets-encryption.test.js +49 -0
  65. package/server/lib/monitoring/alert-manager.js +238 -0
  66. package/server/lib/monitoring/alert-manager.test.js +106 -0
  67. package/server/lib/monitoring/health-check.js +226 -0
  68. package/server/lib/monitoring/health-check.test.js +176 -0
  69. package/server/lib/monitoring/incident-manager.js +230 -0
  70. package/server/lib/monitoring/incident-manager.test.js +98 -0
  71. package/server/lib/monitoring/log-aggregator.js +147 -0
  72. package/server/lib/monitoring/log-aggregator.test.js +89 -0
  73. package/server/lib/monitoring/metrics-collector.js +337 -0
  74. package/server/lib/monitoring/metrics-collector.test.js +172 -0
  75. package/server/lib/monitoring/status-page.js +214 -0
  76. package/server/lib/monitoring/status-page.test.js +105 -0
  77. package/server/lib/monitoring/uptime-monitor.js +194 -0
  78. package/server/lib/monitoring/uptime-monitor.test.js +109 -0
  79. package/server/lib/network/fail2ban-config.js +294 -0
  80. package/server/lib/network/fail2ban-config.test.js +275 -0
  81. package/server/lib/network/firewall-manager.js +252 -0
  82. package/server/lib/network/firewall-manager.test.js +254 -0
  83. package/server/lib/network/geoip-filter.js +282 -0
  84. package/server/lib/network/geoip-filter.test.js +264 -0
  85. package/server/lib/network/rate-limiter.js +229 -0
  86. package/server/lib/network/rate-limiter.test.js +293 -0
  87. package/server/lib/network/request-validator.js +351 -0
  88. package/server/lib/network/request-validator.test.js +345 -0
  89. package/server/lib/network/security-headers.js +251 -0
  90. package/server/lib/network/security-headers.test.js +283 -0
  91. package/server/lib/network/tls-config.js +210 -0
  92. package/server/lib/network/tls-config.test.js +248 -0
  93. package/server/lib/security/auth-security.js +369 -0
  94. package/server/lib/security/auth-security.test.js +448 -0
  95. package/server/lib/security/cis-benchmark.js +152 -0
  96. package/server/lib/security/cis-benchmark.test.js +137 -0
  97. package/server/lib/security/compose-templates.js +312 -0
  98. package/server/lib/security/compose-templates.test.js +229 -0
  99. package/server/lib/security/container-runtime.js +456 -0
  100. package/server/lib/security/container-runtime.test.js +503 -0
  101. package/server/lib/security/cors-validator.js +278 -0
  102. package/server/lib/security/cors-validator.test.js +310 -0
  103. package/server/lib/security/crypto-utils.js +253 -0
  104. package/server/lib/security/crypto-utils.test.js +409 -0
  105. package/server/lib/security/dockerfile-linter.js +459 -0
  106. package/server/lib/security/dockerfile-linter.test.js +483 -0
  107. package/server/lib/security/dockerfile-templates.js +278 -0
  108. package/server/lib/security/dockerfile-templates.test.js +164 -0
  109. package/server/lib/security/error-sanitizer.js +426 -0
  110. package/server/lib/security/error-sanitizer.test.js +331 -0
  111. package/server/lib/security/headers-generator.js +368 -0
  112. package/server/lib/security/headers-generator.test.js +398 -0
  113. package/server/lib/security/image-scanner.js +83 -0
  114. package/server/lib/security/image-scanner.test.js +106 -0
  115. package/server/lib/security/input-validator.js +352 -0
  116. package/server/lib/security/input-validator.test.js +330 -0
  117. package/server/lib/security/network-policy.js +174 -0
  118. package/server/lib/security/network-policy.test.js +164 -0
  119. package/server/lib/security/output-encoder.js +237 -0
  120. package/server/lib/security/output-encoder.test.js +276 -0
  121. package/server/lib/security/path-validator.js +359 -0
  122. package/server/lib/security/path-validator.test.js +293 -0
  123. package/server/lib/security/query-builder.js +421 -0
  124. package/server/lib/security/query-builder.test.js +318 -0
  125. package/server/lib/security/secret-detector.js +290 -0
  126. package/server/lib/security/secret-detector.test.js +354 -0
  127. package/server/lib/security/secrets-validator.js +137 -0
  128. package/server/lib/security/secrets-validator.test.js +120 -0
  129. package/server/lib/security-testing/dast-runner.js +154 -0
  130. package/server/lib/security-testing/dast-runner.test.js +62 -0
  131. package/server/lib/security-testing/dependency-scanner.js +172 -0
  132. package/server/lib/security-testing/dependency-scanner.test.js +64 -0
  133. package/server/lib/security-testing/pentest-runner.js +230 -0
  134. package/server/lib/security-testing/pentest-runner.test.js +60 -0
  135. package/server/lib/security-testing/sast-runner.js +136 -0
  136. package/server/lib/security-testing/sast-runner.test.js +62 -0
  137. package/server/lib/security-testing/secret-scanner.js +153 -0
  138. package/server/lib/security-testing/secret-scanner.test.js +66 -0
  139. package/server/lib/security-testing/security-gate.js +216 -0
  140. package/server/lib/security-testing/security-gate.test.js +115 -0
  141. package/server/lib/security-testing/security-reporter.js +303 -0
  142. package/server/lib/security-testing/security-reporter.test.js +114 -0
  143. package/server/lib/standards/audit-checker.js +546 -0
  144. package/server/lib/standards/audit-checker.test.js +415 -0
  145. package/server/lib/standards/cleanup-executor.js +452 -0
  146. package/server/lib/standards/cleanup-executor.test.js +293 -0
  147. package/server/lib/standards/refactor-stepper.js +425 -0
  148. package/server/lib/standards/refactor-stepper.test.js +298 -0
  149. package/server/lib/standards/standards-injector.js +167 -0
  150. package/server/lib/standards/standards-injector.test.js +232 -0
  151. package/server/lib/user-management.test.js +284 -0
  152. package/server/lib/vps/backup-manager.js +157 -0
  153. package/server/lib/vps/backup-manager.test.js +59 -0
  154. package/server/lib/vps/caddy-config.js +159 -0
  155. package/server/lib/vps/caddy-config.test.js +48 -0
  156. package/server/lib/vps/compose-orchestrator.js +219 -0
  157. package/server/lib/vps/compose-orchestrator.test.js +50 -0
  158. package/server/lib/vps/database-config.js +208 -0
  159. package/server/lib/vps/database-config.test.js +47 -0
  160. package/server/lib/vps/deploy-script.js +211 -0
  161. package/server/lib/vps/deploy-script.test.js +53 -0
  162. package/server/lib/vps/secrets-manager.js +148 -0
  163. package/server/lib/vps/secrets-manager.test.js +58 -0
  164. package/server/lib/vps/server-hardening.js +174 -0
  165. package/server/lib/vps/server-hardening.test.js +70 -0
  166. package/server/package-lock.json +19 -0
  167. package/server/package.json +1 -0
  168. package/server/templates/CLAUDE.md +37 -0
  169. 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
+ '<': '&lt;',
14
+ '>': '&gt;',
15
+ '"': '&quot;',
16
+ "'": '&#x27;',
17
+ '/': '&#x2F;',
18
+ };
19
+
20
+ /**
21
+ * Additional attribute encoding entities
22
+ */
23
+ const ATTRIBUTE_ENTITIES = {
24
+ ...HTML_ENTITIES,
25
+ '=': '&#x3D;',
26
+ '`': '&#x60;',
27
+ '(': '&#x28;',
28
+ ')': '&#x29;',
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
+ }