@xec-sh/core
Version:
Universal shell execution engine
248 lines • 12 kB
JavaScript
import { StreamHandler } from '../utils/stream.js';
import { ProgressReporter } from '../utils/progress.js';
import { TimeoutError, AdapterError } from '../core/error.js';
import { EnhancedEventEmitter } from '../utils/event-emitter.js';
import { ExecutionResultImpl } from '../core/result.js';
export class BaseAdapter extends EnhancedEventEmitter {
constructor(config = {}) {
super();
const defaultPatterns = [
/"(api[_-]?key|apikey|password|token|secret|client[_-]?secret)":\s*"([^"]+)"/gi,
/\b(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|authentication[_-]?token|private[_-]?key|secret[_-]?key)(\s*[:=]\s*)("([^"]+)"|'([^']+)'|([^"'\s]+))/gi,
/(Authorization:\s*)(Bearer|Basic)(\s+)([a-zA-Z0-9_\-/.+=]+)/gi,
/\b(aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret[_-]?access[_-]?key)(\s*[:=]\s*)("([^"]+)"|'([^']+)'|([^"'\s]+))/gi,
/\b(gh[ps]_[a-zA-Z0-9]{16,})/gi,
/\b(github[_-]?token)(\s*[:=]\s*)("([^"]+)"|'([^']+)'|([^"'\s]+))/gi,
/\b(token)(\s*[:=]\s*)("([^"]+)"|'([^']+)'|([^"'\s]+))/gi,
/\b(password|passwd|pwd)(\s*[:=]\s*)("([^"]+)"|'([^']+)'|([^\s]+))/gi,
/(--password)(\s+)("([^"]+)"|'([^']+)'|([^"'\s]+))/gi,
/(--client[_-]?secret|--secret)(\s+)("([^"]+)"|'([^']+)'|([^"'\s]+))/gi,
/-----BEGIN\s+(RSA|DSA|EC|OPENSSH)\s+PRIVATE\s+KEY-----[\s\S]+?-----END\s+(RSA|DSA|EC|OPENSSH)\s+PRIVATE\s+KEY-----/gi,
/\b([A-Z][A-Z0-9_]*(?:SECRET|TOKEN|KEY|PASSWORD|PASSWD|PWD|APIKEY|API_KEY)[A-Z0-9_]*)(\s*[:=]\s*)("([^"]+)"|'([^']+)'|([^\s]+))/gi,
/\b(secret|client[_-]?secret)(\s*[:=]\s*)("([^"]+)"|'([^']+)'|([^"'\s]+))/gi,
/\b(Bearer)(\s+)([a-zA-Z0-9_\-/.]+)/gi
];
this.config = {
defaultTimeout: config.defaultTimeout ?? 120000,
defaultCwd: config.defaultCwd ?? process.cwd(),
defaultEnv: config.defaultEnv ?? {},
defaultShell: config.defaultShell ?? true,
encoding: config.encoding ?? 'utf8',
maxBuffer: config.maxBuffer ?? 10 * 1024 * 1024,
throwOnNonZeroExit: config.throwOnNonZeroExit !== undefined ? config.throwOnNonZeroExit : true,
sensitiveDataMasking: {
enabled: config.sensitiveDataMasking?.enabled ?? true,
patterns: config.sensitiveDataMasking?.patterns ?? defaultPatterns,
replacement: config.sensitiveDataMasking?.replacement ?? '[REDACTED]'
}
};
this.name = '';
}
emitAdapterEvent(event, data) {
if (!this.listenerCount(event))
return;
this.emit(event, {
...data,
timestamp: new Date(),
adapter: this.adapterName
});
}
mergeCommand(command) {
return {
...command,
cwd: command.cwd ?? this.config.defaultCwd,
env: { ...this.config.defaultEnv, ...command.env },
timeout: command.timeout ?? this.config.defaultTimeout,
shell: command.shell ?? this.config.defaultShell,
stdout: command.stdout ?? 'pipe',
stderr: command.stderr ?? 'pipe'
};
}
createStreamHandler(options) {
return new StreamHandler({
encoding: this.config.encoding,
maxBuffer: this.config.maxBuffer,
onData: options?.onData ? (chunk) => {
const maskedChunk = this.maskSensitiveData(chunk);
options.onData(maskedChunk);
} : undefined
});
}
createProgressReporter(command) {
if (!command.progress?.enabled) {
return null;
}
return new ProgressReporter({
enabled: true,
onProgress: command.progress.onProgress,
updateInterval: command.progress.updateInterval,
reportLines: command.progress.reportLines,
prefix: this.adapterName
});
}
async handleTimeout(promise, timeout, command, cleanup) {
if (timeout <= 0) {
return promise;
}
const timeoutPromise = new Promise((_, reject) => {
const timer = setTimeout(() => {
if (cleanup)
cleanup();
reject(new TimeoutError(command, timeout));
}, timeout);
promise.finally(() => clearTimeout(timer));
});
return Promise.race([promise, timeoutPromise]);
}
maskSensitiveData(text) {
if (!this.config.sensitiveDataMasking.enabled || !text) {
return text;
}
let maskedText = text;
for (const pattern of this.config.sensitiveDataMasking.patterns) {
const globalPattern = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g');
maskedText = maskedText.replace(globalPattern, (match, ...args) => {
if (match.includes('BEGIN') && match.includes('PRIVATE KEY')) {
return this.config.sensitiveDataMasking.replacement;
}
if (match.match(/^gh[ps]_[a-zA-Z0-9]{16,}$/)) {
return this.config.sensitiveDataMasking.replacement;
}
const groups = args.slice(0, -2);
if (groups.length === 0 || groups.every(g => g === undefined)) {
return this.config.sensitiveDataMasking.replacement;
}
if (groups.length === 2 && match.includes('":')) {
return `"${groups[0]}": ${this.config.sensitiveDataMasking.replacement}`;
}
if (groups.length === 4 && groups[0] && groups[0].includes('Authorization') && groups[1] && groups[2] !== undefined && groups[3]) {
return groups[0] + groups[1] + ' ' + this.config.sensitiveDataMasking.replacement;
}
if (groups.length === 6) {
const key = groups[0];
const separator = groups[1];
const quotedDouble = groups[3];
const quotedSingle = groups[4];
const unquoted = groups[5];
if (key.startsWith('--')) {
return key + ' ' + this.config.sensitiveDataMasking.replacement;
}
return key + separator + this.config.sensitiveDataMasking.replacement;
}
if (groups.length === 5 && groups[0] && groups[0].startsWith('--')) {
return groups[0] + ' ' + this.config.sensitiveDataMasking.replacement;
}
if (groups.length === 3 && groups[0] && groups[1] !== undefined && groups[2]) {
if (groups[0] === 'Bearer') {
return groups[0] + ' ' + this.config.sensitiveDataMasking.replacement;
}
return groups[0] + groups[1] + this.config.sensitiveDataMasking.replacement;
}
if (groups.length === 2 && groups[0] && groups[1]) {
return groups[0] + this.config.sensitiveDataMasking.replacement;
}
if (groups.length === 1 && groups[0]) {
return this.config.sensitiveDataMasking.replacement;
}
if (match.includes('=')) {
const [key,] = match.split('=', 2);
return key + '=' + this.config.sensitiveDataMasking.replacement;
}
else if (match.includes(':')) {
const [key,] = match.split(':', 2);
return key + ': ' + this.config.sensitiveDataMasking.replacement;
}
return this.config.sensitiveDataMasking.replacement;
});
}
return maskedText;
}
createResultSync(stdout, stderr, exitCode, signal, command, startTime, endTime, context) {
const maskedStdout = this.maskSensitiveData(stdout);
const maskedStderr = this.maskSensitiveData(stderr);
const maskedCommand = this.maskSensitiveData(command);
const result = new ExecutionResultImpl(maskedStdout, maskedStderr, exitCode, signal, maskedCommand, endTime - startTime, new Date(startTime), new Date(endTime), this.adapterName, context?.host, context?.container);
const commandForThrowCheck = context?.originalCommand ?? command;
if (this.shouldThrowOnNonZeroExit(commandForThrowCheck, exitCode)) {
result.throwIfFailed();
}
return result;
}
async createResult(stdout, stderr, exitCode, signal, command, startTime, endTime, context) {
const maskedStdout = this.maskSensitiveData(stdout);
const maskedStderr = this.maskSensitiveData(stderr);
const maskedCommand = this.maskSensitiveData(command);
const result = new ExecutionResultImpl(maskedStdout, maskedStderr, exitCode, signal, maskedCommand, endTime - startTime, new Date(startTime), new Date(endTime), this.adapterName, context?.host, context?.container);
const commandForThrowCheck = context?.originalCommand ?? command;
if (this.shouldThrowOnNonZeroExit(commandForThrowCheck, exitCode)) {
result.throwIfFailed();
}
return result;
}
shouldThrowOnNonZeroExit(command, exitCode) {
if (exitCode === 0) {
return false;
}
if (typeof command === 'string') {
return this.config.throwOnNonZeroExit;
}
if (command.nothrow !== undefined) {
return !command.nothrow;
}
return this.config.throwOnNonZeroExit;
}
buildCommandString(command) {
if (command.args && command.args.length > 0) {
return `${command.command} ${command.args.join(' ')}`;
}
return command.command;
}
async handleAbortSignal(signal, cleanup) {
if (!signal)
return;
if (signal.aborted) {
cleanup();
throw new AdapterError(this.adapterName, 'execute', new Error('Operation aborted'));
}
const abortHandler = () => {
cleanup();
};
signal.addEventListener('abort', abortHandler, { once: true });
}
createCombinedEnv(baseEnv, commandEnv) {
const combined = {};
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined) {
combined[key] = value;
}
}
Object.assign(combined, baseEnv);
if (commandEnv) {
Object.assign(combined, commandEnv);
}
return combined;
}
updateConfig(config) {
const newSensitiveDataMasking = config.sensitiveDataMasking
? {
enabled: config.sensitiveDataMasking.enabled ?? this.config.sensitiveDataMasking.enabled,
patterns: config.sensitiveDataMasking.patterns ?? this.config.sensitiveDataMasking.patterns,
replacement: config.sensitiveDataMasking.replacement ?? this.config.sensitiveDataMasking.replacement
}
: this.config.sensitiveDataMasking;
this.config = {
defaultTimeout: config.defaultTimeout ?? this.config.defaultTimeout,
defaultCwd: config.defaultCwd ?? this.config.defaultCwd,
defaultEnv: config.defaultEnv ?? this.config.defaultEnv,
defaultShell: config.defaultShell ?? this.config.defaultShell,
encoding: config.encoding ?? this.config.encoding,
maxBuffer: config.maxBuffer ?? this.config.maxBuffer,
throwOnNonZeroExit: config.throwOnNonZeroExit !== undefined ? config.throwOnNonZeroExit : this.config.throwOnNonZeroExit,
sensitiveDataMasking: newSensitiveDataMasking
};
}
getConfig() {
return { ...this.config };
}
}
//# sourceMappingURL=base-adapter.js.map