@archznn/xavva 3.1.2 → 3.2.0
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/README.md +221 -12
- package/package.json +3 -2
- package/src/commands/AuditCommand.ts +12 -10
- package/src/commands/BuildCommand.ts +9 -7
- package/src/commands/ChangelogCommand.ts +5 -5
- package/src/commands/CleanCommand.ts +242 -0
- package/src/commands/CompletionCommand.ts +7 -7
- package/src/commands/DbCommand.ts +43 -14
- package/src/commands/DeployCommand.ts +252 -229
- package/src/commands/DepsCommand.ts +174 -174
- package/src/commands/DockerCommand.ts +35 -4
- package/src/commands/DoctorCommand.ts +252 -239
- package/src/commands/EncodingCommand.ts +26 -19
- package/src/commands/HealthCommand.ts +7 -7
- package/src/commands/HelpCommand.ts +34 -14
- package/src/commands/HistoryCommand.ts +5 -5
- package/src/commands/HttpCommand.ts +27 -1
- package/src/commands/IdeCommand.ts +313 -0
- package/src/commands/InitCommand.ts +26 -25
- package/src/commands/LogsCommand.ts +8 -6
- package/src/commands/ProfilesCommand.ts +6 -6
- package/src/commands/RedoCommand.ts +2 -2
- package/src/commands/RunCommand.ts +64 -24
- package/src/commands/StartCommand.ts +9 -7
- package/src/commands/TestCommand.ts +25 -1
- package/src/commands/TomcatCommand.ts +232 -88
- package/src/config/versions.ts +111 -9
- package/src/di/container.ts +239 -105
- package/src/errors/ErrorHandler.ts +23 -19
- package/src/errors/errorMessages.ts +235 -0
- package/src/index.ts +20 -6
- package/src/logging/FileLogger.ts +235 -0
- package/src/logging/Logger.ts +545 -0
- package/src/logging/OperationLogger.ts +296 -0
- package/src/logging/ProgressLogger.ts +187 -0
- package/src/logging/TableLogger.ts +246 -0
- package/src/logging/colors.ts +167 -0
- package/src/logging/constants.ts +176 -0
- package/src/logging/formatters.ts +337 -0
- package/src/logging/index.ts +93 -0
- package/src/logging/types.ts +64 -0
- package/src/plugins/PluginManager.ts +325 -0
- package/src/plugins/types.ts +82 -0
- package/src/services/AuditService.ts +5 -3
- package/src/services/BuildService.ts +15 -17
- package/src/services/DashboardService.ts +14 -3
- package/src/services/DbService.ts +35 -34
- package/src/services/DependencyAnalyzerService.ts +18 -18
- package/src/services/DependencyCacheService.ts +303 -0
- package/src/services/DeployWatcher.ts +127 -23
- package/src/services/DockerService.ts +3 -3
- package/src/services/EmbeddedTomcatService.ts +13 -12
- package/src/services/FileWatcher.ts +15 -7
- package/src/services/HttpService.ts +5 -5
- package/src/services/LogAnalyzer.ts +26 -22
- package/src/services/PerformanceProfiler.ts +267 -0
- package/src/services/ProjectService.ts +3 -0
- package/src/services/TestService.ts +3 -3
- package/src/services/TomcatService.ts +46 -25
- package/src/services/tomcat/TomcatBackupManager.ts +330 -0
- package/src/services/tomcat/TomcatChecksumVerifier.ts +211 -0
- package/src/services/tomcat/TomcatCompatibilityChecker.ts +298 -0
- package/src/services/tomcat/TomcatDownloadCache.ts +250 -0
- package/src/services/tomcat/TomcatDownloadService.ts +335 -0
- package/src/services/tomcat/TomcatInstallerService.ts +474 -0
- package/src/services/tomcat/TomcatMirrorManager.ts +181 -0
- package/src/services/tomcat/index.ts +36 -0
- package/src/services/tomcat/types.ts +120 -0
- package/src/types/args.ts +68 -1
- package/src/types/configSchema.ts +174 -0
- package/src/utils/ChangelogGenerator.ts +11 -11
- package/src/utils/LoggerLevel.ts +44 -20
- package/src/utils/ProgressBar.ts +87 -46
- package/src/utils/argsParser.ts +260 -0
- package/src/utils/config.ts +340 -189
- package/src/utils/constants.ts +87 -9
- package/src/utils/dryRun.ts +192 -0
- package/src/utils/processManager.ts +23 -7
- package/src/utils/security.ts +293 -0
- package/src/utils/ui.ts +299 -428
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilitários de Segurança
|
|
3
|
+
*
|
|
4
|
+
* - Sanitização de inputs
|
|
5
|
+
* - Validação de paths (path traversal prevention)
|
|
6
|
+
* - Sanitização de argumentos de shell
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
import { Logger } from "../logging";
|
|
12
|
+
|
|
13
|
+
export class SecurityError extends Error {
|
|
14
|
+
constructor(message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "SecurityError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sanitiza path para prevenir path traversal
|
|
22
|
+
*/
|
|
23
|
+
export function sanitizePath(input: string, baseDir: string = process.cwd()): string {
|
|
24
|
+
// Remove null bytes
|
|
25
|
+
if (input.includes('\0')) {
|
|
26
|
+
throw new SecurityError("Path contém caracteres nulos");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Normaliza o path
|
|
30
|
+
const normalized = path.normalize(input);
|
|
31
|
+
|
|
32
|
+
// Resolve para path absoluto
|
|
33
|
+
const resolved = path.resolve(baseDir, normalized);
|
|
34
|
+
|
|
35
|
+
// Garante que está dentro do diretório base
|
|
36
|
+
const resolvedBase = path.resolve(baseDir);
|
|
37
|
+
if (!resolved.startsWith(resolvedBase)) {
|
|
38
|
+
throw new SecurityError(`Path traversal detectado: ${input}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Sanitiza argumentos de shell
|
|
46
|
+
*/
|
|
47
|
+
export function sanitizeShellArg(input: string): string {
|
|
48
|
+
// Remove caracteres perigosos
|
|
49
|
+
const dangerous = /[;&|`$(){}[\]\\<>!#*?]/g;
|
|
50
|
+
|
|
51
|
+
if (dangerous.test(input)) {
|
|
52
|
+
throw new SecurityError(`Argumento contém caracteres perigosos: ${input}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return input;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Valida nome de arquivo
|
|
60
|
+
*/
|
|
61
|
+
export function validateFilename(filename: string): string {
|
|
62
|
+
// Caracteres proibidos em nomes de arquivo
|
|
63
|
+
const forbidden = /[<>:"/\\|?*\0]/;
|
|
64
|
+
|
|
65
|
+
if (forbidden.test(filename)) {
|
|
66
|
+
throw new SecurityError(`Nome de arquivo inválido: ${filename}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Não permite paths absolutos
|
|
70
|
+
if (path.isAbsolute(filename)) {
|
|
71
|
+
throw new SecurityError(`Caminho absoluto não permitido: ${filename}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Não permite .. ou .
|
|
75
|
+
if (filename.includes('..') || filename === '.') {
|
|
76
|
+
throw new SecurityError(`Path traversal detectado: ${filename}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return filename;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Valida porta
|
|
84
|
+
*/
|
|
85
|
+
export function validatePortNumber(port: number | string): number {
|
|
86
|
+
const num = typeof port === 'string' ? parseInt(port, 10) : port;
|
|
87
|
+
|
|
88
|
+
if (isNaN(num) || num < 1 || num > 65535) {
|
|
89
|
+
throw new SecurityError(`Porta inválida: ${port}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Portas privilegiadas (1-1024) requerem root
|
|
93
|
+
if (num < 1024) {
|
|
94
|
+
throw new SecurityError(`Portas privilegiadas (1-1024) não permitidas: ${port}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return num;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Valida URL
|
|
102
|
+
*/
|
|
103
|
+
export function validateUrl(url: string, allowedProtocols: string[] = ['http:', 'https:']): string {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = new URL(url);
|
|
106
|
+
|
|
107
|
+
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
108
|
+
throw new SecurityError(`Protocolo não permitido: ${parsed.protocol}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Bloqueia localhost em produção (opcional)
|
|
112
|
+
if (process.env.NODE_ENV === 'production') {
|
|
113
|
+
const blockedHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1'];
|
|
114
|
+
if (blockedHosts.includes(parsed.hostname)) {
|
|
115
|
+
throw new SecurityError(`Host não permitido: ${parsed.hostname}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return url;
|
|
120
|
+
} catch (e) {
|
|
121
|
+
if (e instanceof SecurityError) throw e;
|
|
122
|
+
throw new SecurityError(`URL inválida: ${url}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Computa hash SHA-256 de arquivo
|
|
128
|
+
*/
|
|
129
|
+
export async function computeFileHash(filePath: string): Promise<string> {
|
|
130
|
+
const file = Bun.file(filePath);
|
|
131
|
+
const buffer = await file.arrayBuffer();
|
|
132
|
+
const hash = createHash('sha256').update(Buffer.from(buffer)).digest('hex');
|
|
133
|
+
return hash;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Verifica checksum de arquivo
|
|
138
|
+
*/
|
|
139
|
+
export async function verifyChecksum(
|
|
140
|
+
filePath: string,
|
|
141
|
+
expectedHash: string,
|
|
142
|
+
algorithm: 'sha256' | 'sha512' = 'sha256'
|
|
143
|
+
): Promise<boolean> {
|
|
144
|
+
const logger = Logger.getInstance();
|
|
145
|
+
|
|
146
|
+
logger.debug(`Verificando checksum de ${path.basename(filePath)}`);
|
|
147
|
+
|
|
148
|
+
const file = Bun.file(filePath);
|
|
149
|
+
const buffer = await file.arrayBuffer();
|
|
150
|
+
const hash = createHash(algorithm).update(Buffer.from(buffer)).digest('hex');
|
|
151
|
+
|
|
152
|
+
if (hash !== expectedHash.toLowerCase()) {
|
|
153
|
+
logger.error(`Checksum mismatch!`);
|
|
154
|
+
logger.error(` Esperado: ${expectedHash}`);
|
|
155
|
+
logger.error(` Obtido: ${hash}`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
logger.debug(`Checksum verificado com sucesso`);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Verifica checksum de arquivo baixado
|
|
165
|
+
*/
|
|
166
|
+
export async function verifyDownloadChecksum(
|
|
167
|
+
filePath: string,
|
|
168
|
+
checksumUrl: string,
|
|
169
|
+
algorithm: 'sha256' | 'sha512' = 'sha256'
|
|
170
|
+
): Promise<boolean> {
|
|
171
|
+
const logger = Logger.getInstance();
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
logger.debug(`Baixando checksum de ${checksumUrl}`);
|
|
175
|
+
|
|
176
|
+
const response = await fetch(checksumUrl);
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
logger.warn(`Não foi possível baixar checksum: ${response.status}`);
|
|
179
|
+
return true; // Permite continuar sem checksum
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const checksumContent = await response.text();
|
|
183
|
+
const expectedHash = checksumContent.trim().split(/\s+/)[0];
|
|
184
|
+
|
|
185
|
+
return await verifyChecksum(filePath, expectedHash, algorithm);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.warn(`Erro ao verificar checksum: ${(error as Error).message}`);
|
|
188
|
+
return true; // Permite continuar em caso de erro
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Escapa string para uso em regex
|
|
194
|
+
*/
|
|
195
|
+
export function escapeRegex(string: string): string {
|
|
196
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Valida entrada de usuário (básica)
|
|
201
|
+
*/
|
|
202
|
+
export function validateUserInput(input: string, maxLength: number = 1000): string {
|
|
203
|
+
if (typeof input !== 'string') {
|
|
204
|
+
throw new SecurityError("Input deve ser uma string");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (input.length > maxLength) {
|
|
208
|
+
throw new SecurityError(`Input excede tamanho máximo de ${maxLength} caracteres`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Remove caracteres de controle (exceto whitespace)
|
|
212
|
+
const sanitized = input.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
213
|
+
|
|
214
|
+
return sanitized;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Sanitiza variáveis de ambiente
|
|
219
|
+
*/
|
|
220
|
+
export function sanitizeEnvVar(name: string, value: string): { name: string; value: string } {
|
|
221
|
+
// Nomes de variáveis devem seguir [a-zA-Z_][a-zA-Z0-9_]*
|
|
222
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
223
|
+
throw new SecurityError(`Nome de variável de ambiente inválido: ${name}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Remove caracteres nulos
|
|
227
|
+
const cleanValue = value.replace(/\0/g, '');
|
|
228
|
+
|
|
229
|
+
return { name, value: cleanValue };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Wrapper seguro para execução de comandos
|
|
234
|
+
*/
|
|
235
|
+
export function createSafeCommand(
|
|
236
|
+
command: string,
|
|
237
|
+
args: string[]
|
|
238
|
+
): { command: string; args: string[] } {
|
|
239
|
+
// Sanitiza cada argumento
|
|
240
|
+
const sanitizedArgs = args.map(arg => {
|
|
241
|
+
// Se contém espaços, precisa de quotes
|
|
242
|
+
if (arg.includes(' ')) {
|
|
243
|
+
// Verifica se já está quoted
|
|
244
|
+
if ((arg.startsWith('"') && arg.endsWith('"')) ||
|
|
245
|
+
(arg.startsWith("'") && arg.endsWith("'"))) {
|
|
246
|
+
return arg;
|
|
247
|
+
}
|
|
248
|
+
// Escapa quotes internos e envolve
|
|
249
|
+
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
250
|
+
}
|
|
251
|
+
return sanitizeShellArg(arg);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return { command: sanitizeShellArg(command), args: sanitizedArgs };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Rate limiting simples
|
|
259
|
+
*/
|
|
260
|
+
export class RateLimiter {
|
|
261
|
+
private requests: Map<string, number[]> = new Map();
|
|
262
|
+
private windowMs: number;
|
|
263
|
+
private maxRequests: number;
|
|
264
|
+
|
|
265
|
+
constructor(windowMs: number = 60000, maxRequests: number = 100) {
|
|
266
|
+
this.windowMs = windowMs;
|
|
267
|
+
this.maxRequests = maxRequests;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
isAllowed(key: string): boolean {
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
const timestamps = this.requests.get(key) || [];
|
|
273
|
+
|
|
274
|
+
// Remove timestamps antigos
|
|
275
|
+
const validTimestamps = timestamps.filter(t => now - t < this.windowMs);
|
|
276
|
+
|
|
277
|
+
if (validTimestamps.length >= this.maxRequests) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
validTimestamps.push(now);
|
|
282
|
+
this.requests.set(key, validTimestamps);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
reset(key?: string): void {
|
|
287
|
+
if (key) {
|
|
288
|
+
this.requests.delete(key);
|
|
289
|
+
} else {
|
|
290
|
+
this.requests.clear();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|