Viewing File: /usr/local/cpanel/whostmgr/docroot/cgi/softaculous/lib/ai/core/class_tool_executor.php
<?php
if(!defined('SOFTACULOUS')){
die('Hacking Attempt');
}
interface AIIsolationProvider {
public function isAvailable();
public function getId();
public function wrapCommand($command);
public function wrapPhpCommand($php_cmd);
public function isConfined();
}
class AICageFsIsolation implements AIIsolationProvider {
private $cagefs_enter;
private $available = null;
private static $static_cache = array();
public function __construct(){
$this->cagefs_enter = '/bin/cagefs_enter';
}
public function getId(){
return 'cagefs';
}
public function isAvailable(){
if($this->available !== null){
return $this->available;
}
$cache_key = 'cagefs';
if(isset(self::$static_cache[$cache_key])){
$this->available = self::$static_cache[$cache_key];
return $this->available;
}
$this->available = false;
if(!file_exists($this->cagefs_enter) || !is_executable($this->cagefs_enter)){
self::$static_cache[$cache_key] = false;
return false;
}
if(!$this->isCagefsActive()){
self::$static_cache[$cache_key] = false;
return false;
}
$this->available = true;
self::$static_cache[$cache_key] = true;
return true;
}
private function isCagefsActive(){
$mountinfo = @file_get_contents('/proc/self/mountinfo');
if($mountinfo === false){
return false;
}
return strpos($mountinfo, 'cagefs') !== false;
}
public function wrapCommand($command){
return escapeshellarg($this->cagefs_enter) . ' /bin/bash -c ' . escapeshellarg($command);
}
public function wrapPhpCommand($php_cmd){
return escapeshellarg($this->cagefs_enter) . ' /bin/bash -c ' . escapeshellarg($php_cmd);
}
public function isConfined(){
return true;
}
}
class AIJailIsolation implements AIIsolationProvider {
private $jail_script;
private $user_home_dir;
private $available = null;
public function __construct($jail_script, $user_home_dir){
$this->jail_script = $jail_script;
$this->user_home_dir = rtrim($user_home_dir, '/');
}
public function getId(){
return 'jail';
}
public function isAvailable(){
if($this->available !== null){
return $this->available;
}
$this->available = false;
if(!file_exists($this->jail_script) || !is_executable($this->jail_script)){
return false;
}
$test_cmd = sprintf('%s %s -- echo JAIL_TEST_OK 2>/dev/null',
escapeshellarg($this->jail_script),
escapeshellarg($this->user_home_dir)
);
$output = shell_exec($test_cmd);
if(strpos($output, 'JAIL_TEST_OK') !== false){
$this->available = true;
}
return $this->available;
}
public function wrapCommand($command){
return sprintf('%s %s -- bash -c %s',
escapeshellarg($this->jail_script),
escapeshellarg($this->user_home_dir),
escapeshellarg('cd ' . escapeshellarg($this->user_home_dir) . ' && ' . $command)
);
}
public function wrapPhpCommand($php_cmd){
return sprintf('%s %s -- %s',
escapeshellarg($this->jail_script),
escapeshellarg($this->user_home_dir),
$php_cmd
);
}
public function isConfined(){
return true;
}
}
class AINoIsolation implements AIIsolationProvider {
public function isAvailable(){
return true;
}
public function getId(){
return 'none';
}
public function wrapCommand($command){
return $command;
}
public function wrapPhpCommand($php_cmd){
return $php_cmd;
}
public function isConfined(){
return false;
}
}
class ToolExecutor {
private $file_manager;
private $project_path;
private $user_home_dir;
private $mode = 'build';
private $abort_file = '';
private $isolation;
private $process_user;
private $permissions = array();
private $username = '';
public function __construct(AIFileManager $file_manager, $project_path, $user_home_dir = null, $mode = 'build', $username = ''){
global $globals;
$this->file_manager = $file_manager;
$this->project_path = rtrim($project_path, '/');
$this->user_home_dir = $user_home_dir ? rtrim($user_home_dir, '/') : $this->project_path;
$this->mode = $mode;
$this->username = (string)$username;
$this->process_user = $this->resolveProcessUser();
$providers = $this->buildIsolationProviders($globals);
foreach($providers as $provider){
if($provider->isAvailable()){
$this->isolation = $provider;
break;
}
}
if(empty($this->isolation)){
$this->isolation = new AINoIsolation();
}
# $this->debug_log_isolation();
}
public function set_permissions(array $permissions){
$this->permissions = $permissions;
}
/**
* Detects if $path refers to a project context file (.softaculous-ai/project-bootstrap.md,
* project-state.md, project-memory.md) OR a simple context filename (project-bootstrap.md,
* project-state.md, project-memory.md). Returns the canonical context filename
* (e.g. "project-bootstrap.md") or '' if not a context path.
*/
private function is_context_file($path){
$path = (string)$path;
$ctx_files = array('project-bootstrap.md', 'project-state.md', 'project-memory.md');
// Normalise: strip leading ./
$norm = $path;
if(substr($norm, 0, 2) === './'){
$norm = substr($norm, 2);
}
// Strip absolute project_path prefix if present
$pp_full = $this->project_path . '/.softaculous-ai/';
if(strpos($path, $pp_full) === 0){
$norm = '.softaculous-ai/' . substr($path, strlen($pp_full));
}
if(strpos($norm, '.softaculous-ai/') === 0){
$name = substr($norm, strlen('.softaculous-ai/'));
$name = trim($name, '/');
if(in_array($name, $ctx_files, true)){
return $name;
}
}
// Also accept bare context filenames (e.g. "project-bootstrap.md")
if(in_array($norm, $ctx_files, true)){
return $norm;
}
return '';
}
/**
* Reads a context file from the persistent per-user context directory.
* Returns the content or '' if not found.
*/
private function read_context_file($name){
if(empty($name) || !function_exists('ai_php_load_ctx_file')) return '';
return ai_php_load_ctx_file($this->project_path, $name, $this->username);
}
/**
* Writes a context file directly to the persistent per-user context directory
* (~/.softaculous/ai/context/{project_id}/). The project root is never touched
* so no .softaculous-ai/ folder is created inside the installation.
*/
private function write_context_file($name, $content){
if(empty($name) || !function_exists('ai_php_save_ctx_file')) return false;
return ai_php_save_ctx_file($this->project_path, $name, (string)$content, $this->username);
}
private function resolveProcessUser(){
if(function_exists('posix_getpwuid') && function_exists('posix_getuid')){
$pw = posix_getpwuid(posix_getuid());
if(is_array($pw) && !empty($pw['name'])){
return $pw['name'];
}
}
return '';
}
private function debug_log_isolation(){
$log_dir = $this->user_home_dir . '/.softaculous/ai';
if(!is_dir($log_dir)){
@mkdir($log_dir, 0711, true);
}
@chmod($log_dir, 0711);
$log_file = $log_dir . '/isolation_debug.log';
$timestamp = date('Y-m-d H:i:s');
$iso_id = $this->isolation->getId();
$confined = $this->isolation->isConfined() ? 'yes' : 'no';
$username = $this->process_user;
$details = '';
if($iso_id === 'cagefs'){
$details = 'cagefs_enter=' . (file_exists('/bin/cagefs_enter') && is_executable('/bin/cagefs_enter') ? 'exists' : 'missing');
$mountinfo = @file_get_contents('/proc/self/mountinfo');
$details .= ' cagefs_mount=' . (strpos($mountinfo, 'cagefs') !== false ? 'yes' : 'no');
}elseif($iso_id === 'jail'){
$jail = '';
global $globals;
if(!empty($globals['path'])) $jail = $globals['path'].'/bin/ai_jail.sh';
$details = 'jail_script=' . ($jail && file_exists($jail) && is_executable($jail) ? 'exists' : 'missing');
}else{
$details = 'cagefs_enter=' . (file_exists('/bin/cagefs_enter') ? 'exists' : 'missing');
$mountinfo = @file_get_contents('/proc/self/mountinfo');
$details .= ' cagefs_mount=' . (strpos($mountinfo, 'cagefs') !== false ? 'yes' : 'no');
$jail = '';
if(!empty($globals['path'])) $jail = $globals['path'].'/bin/ai_jail.sh';
$details .= ' jail_script=' . ($jail && file_exists($jail) ? 'exists' : 'missing');
$details .= ' max_user_namespaces=' . @file_get_contents('/proc/sys/user/max_user_namespaces');
}
$line = "[{$timestamp}] isolation={$iso_id} confined={$confined} user={$username} project={$this->project_path} {$details}\n";
@file_put_contents($log_file, $line, FILE_APPEND | LOCK_EX);
@chmod($log_file, 0600);
}
private function buildIsolationProviders($globals){
$providers = array();
$providers[] = new AICageFsIsolation();
$jail_script = '';
if(!empty($globals['path'])){
$jail_script = $globals['path'].'/bin/ai_jail.sh';
}
if(!empty($jail_script)){
$providers[] = new AIJailIsolation($jail_script, $this->user_home_dir);
}
return $providers;
}
public function set_abort_file($filepath){
$this->abort_file = $filepath;
}
private function should_abort(){
if(empty($this->abort_file)) return false;
if(file_exists($this->abort_file)) return true;
if(function_exists('connection_aborted') && connection_aborted()) return true;
return false;
}
public function execute($tool_name, array $params){
if($this->should_abort()){
return array('output' => __('Aborted by user'), 'is_error' => true);
}
$read_only_tools = array('read_file', 'glob', 'grep', 'list_directory', 'web_fetch', 'todo_write', 'question', 'php_lint');
if($this->mode === 'plan' && !in_array($tool_name, $read_only_tools)){
return array('output' => __('Permission denied: write operations are not allowed in Plan mode.'), 'is_error' => true);
}
$write_tools = array('write_file', 'edit_file', 'bash', 'file_download', 'archive', 'search_replace', 'apply_patch');
if(in_array($tool_name, $write_tools)){
$perm_key = $tool_name;
if(isset($this->permissions[$perm_key])){
$perm_val = $this->permissions[$perm_key];
if($perm_val === 'deny'){
return array('output' => __('Permission denied: $0 is not allowed. You can change this in Settings > Permissions.', array($tool_name)), 'is_error' => true, '_permission_deny' => true);
}
}
}
$read_only_tools = array('read_file', 'glob', 'grep', 'list_directory', 'web_fetch', 'todo_write', 'question', 'php_lint');
if($this->mode === 'plan' && !in_array($tool_name, $read_only_tools)){
return array('output' => __('Permission denied: write operations are not allowed in Plan mode.'), 'is_error' => true);
}
$write_tools = array('write_file', 'edit_file', 'bash', 'file_download', 'archive', 'search_replace', 'apply_patch');
if(in_array($tool_name, $write_tools)){
$perm_key = $tool_name;
if(isset($this->permissions[$perm_key])){
$perm_val = $this->permissions[$perm_key];
if($perm_val === 'deny'){
return array('output' => __('Permission denied: $0 is not allowed. You can change this in Settings > Permissions.', array($tool_name)), 'is_error' => true, '_permission_deny' => true);
}
if($perm_val === 'ask'){
return array('output' => __('Permission required for $0', array($tool_name)), 'is_error' => false, '_permission_ask' => true);
}
}
}
switch($tool_name){
case 'read_file': return $this->tool_read_file($params);
case 'write_file': return $this->tool_write_file($params);
case 'edit_file': return $this->tool_edit_file($params);
case 'bash': return $this->tool_bash($params);
case 'glob': return $this->tool_glob($params);
case 'grep': return $this->tool_grep($params);
case 'list_directory': return $this->tool_list_directory($params);
case 'web_fetch': return $this->tool_web_fetch($params);
case 'todo_write': return $this->tool_todo_write($params);
case 'file_download': return $this->tool_file_download($params);
case 'archive': return $this->tool_archive($params);
case 'search_replace': return $this->tool_search_replace($params);
case 'php_eval': return $this->tool_php_eval($params);
case 'php_lint': return $this->tool_php_lint($params);
case 'apply_patch': return $this->tool_apply_patch($params);
case 'question': return $this->tool_question($params);
default: return array('output' => __('Unknown tool: $0', array($tool_name)), 'is_error' => true);
}
}
private function tool_read_file(array $params){
$path = isset($params['path']) ? $params['path'] : '';
$offset = isset($params['offset']) ? intval($params['offset']) : 1;
$limit = isset($params['limit']) ? intval($params['limit']) : 2000;
// We do not allow it to read the settings file.
if(strpos($path, 'ai/settings.json.php') != 0){
return array('output' => __('Access denied'), 'is_error' => true);
}
// For context files, read from the persistent context dir
$ctx_name = $this->is_context_file($path);
if($ctx_name){
$content = $this->read_context_file($ctx_name);
if($content === ''){
return array('output' => __('File not found'), 'is_error' => true);
}
}else{
$result = $this->file_manager->read_file($path);
if(!empty($result['error'])){
return array('output' => $result['error'], 'is_error' => true);
}
$content = $result['content'];
}
$lines = explode("\n", $content);
if($offset > 1 || $limit < count($lines)){
$lines = array_slice($lines, max(0, $offset - 1), $limit);
}
$output = '';
$line_no = max(1, $offset);
foreach($lines as $line){
$output .= str_pad($line_no, 5, ' ', STR_PAD_LEFT) . ' | ' . $line . "\n";
$line_no++;
}
return array('output' => rtrim($output), 'is_error' => false);
}
private function tool_write_file(array $params){
$path = isset($params['path']) ? $params['path'] : '';
$content = isset($params['content']) ? $params['content'] : '';
if(empty($path)){
return array('output' => __('Path is required'), 'is_error' => true);
}
// If this is a context file (.softaculous-ai/project-*.md), write it
// directly to the persistent per-user context directory without ever
// touching the project root (no .softaculous-ai/ folder is created).
$ctx_name = $this->is_context_file($path);
$original = '';
if($ctx_name){
$original = $this->read_context_file($ctx_name);
if(!$this->write_context_file($ctx_name, $content)){
return array('output' => __('Failed to save context file'), 'is_error' => true);
}
}else{
$existing = $this->file_manager->read_file($path);
if(!empty($existing['content'])) $original = $existing['content'];
$result = $this->file_manager->write_file($path, $content, false);
if(!empty($result['error'])){
return array('output' => $result['error'], 'is_error' => true);
}
}
$lines = substr_count($content, "\n") + 1;
$display_path = $ctx_name ? $ctx_name : $path;
$return = array('output' => __('Successfully wrote $0 bytes ($1 lines) to $2', array(strlen($content), $lines, $display_path)), 'is_error' => false);
if($original !== '' && $original !== $content){
$return['diff'] = $this->compute_unified_diff($original, $content);
}
return $return;
}
private function tool_edit_file(array $params){
$path = isset($params['path']) ? $params['path'] : '';
$old_string = isset($params['old_string']) ? $params['old_string'] : '';
$new_string = isset($params['new_string']) ? $params['new_string'] : '';
$replace_all = !empty($params['replace_all']);
if(empty($path) || empty($old_string)){
return array('output' => __('Path and old_string are required'), 'is_error' => true);
}
// For context files, read from the persistent context dir
$ctx_name = $this->is_context_file($path);
if($ctx_name){
$content = $this->read_context_file($ctx_name);
}else{
$result = $this->file_manager->read_file($path);
if(!empty($result['error'])){
return array('output' => $result['error'], 'is_error' => true);
}
$content = $result['content'];
}
if(strpos($content, $old_string) === false){
$fuzzy_result = $this->fuzzy_find($content, $old_string);
if($fuzzy_result !== false){
$old_string = $fuzzy_result;
}else{
$snippet = mb_substr($old_string, 0, 100);
return array('output' => __('Could not find the specified text in $0. Searched for: "$1..."', array($path, $snippet)), 'is_error' => true);
}
}
$count = 0;
if($replace_all){
$new_content = str_replace($old_string, $new_string, $content, $count);
}else{
$pos = strpos($content, $old_string);
if($pos !== false){
$new_content = substr($content, 0, $pos) . $new_string . substr($content, $pos + strlen($old_string));
$count = 1;
}else{
$new_content = $content;
}
}
if($count === 0){
return array('output' => __('No replacements made in $0', array($path)), 'is_error' => true);
}
// Context files live in the persistent per-user context directory, so
// persist the edit there instead of the project root.
if($ctx_name){
if(!$this->write_context_file($ctx_name, $new_content)){
return array('output' => __('Failed to save context file'), 'is_error' => true);
}
}else{
$result2 = $this->file_manager->write_file($path, $new_content, false);
if(!empty($result2['error'])){
return array('output' => $result2['error'], 'is_error' => true);
}
}
$diff = $this->make_mini_diff($old_string, $new_string);
$display_path = $ctx_name ? $ctx_name : $path;
$return = array('output' => __('Edited $0 ($1 replacement(s))', array($display_path, $count))."\n{$diff}", 'is_error' => false);
$return['diff'] = $this->compute_unified_diff($content, $new_content);
return $return;
}
private function tool_bash(array $params){
$command = isset($params['command']) ? $params['command'] : '';
$timeout = isset($params['timeout']) ? intval($params['timeout']) : 30;
$timeout = max(5, min($timeout, 120));
if(empty($command)){
return array('output' => __('Command is required'), 'is_error' => true);
}
// Respect the OS / control-panel login-shell policy. If the account
// does not have shell access (nologin / noshell / false), the bash
// tool is disabled too — otherwise it would bypass the account's
// shell restriction, which is exactly the policy the AI shell feature
// must not undermine.
if(!empty($this->username) && function_exists('ai_user_has_shell_access') && !ai_user_has_shell_access($this->username)){
return array('output' => __('Command execution is disabled because shell access is not enabled for this account.'), 'is_error' => true);
}
if($this->is_dangerous_command($command)){
return array('output' => __('This command is blocked for safety reasons.'), 'is_error' => true);
}
if(!$this->isolation->isConfined()){
return array('output' => __('Command execution is disabled: no isolation mechanism available.'), 'is_error' => true);
}
$env = array();
if(!empty($this->user_home_dir)){
$env['HOME'] = $this->user_home_dir;
}
$env['PATH'] = '/usr/local/bin:/usr/bin:/bin';
$env['TERM'] = 'dumb';
$wrapped = $this->isolation->wrapCommand('cd ' . escapeshellarg($this->project_path) . ' && ' . $command);
return $this->run_process($wrapped, $timeout, $env);
}
private function tool_glob(array $params){
$pattern = isset($params['pattern']) ? $params['pattern'] : '**/*';
$base_path = isset($params['path']) ? $params['path'] : '';
if(!$this->isolation->isConfined()){
return array('output' => __('File search is disabled: no isolation mechanism available.'), 'is_error' => true);
}
$full_base = $this->project_path;
if(!empty($base_path)){
$resolved = $this->file_manager->resolve_path($base_path);
if(strpos($resolved, $this->user_home_dir) === 0){
$full_base = $resolved;
}
}
$pattern = escapeshellarg($pattern);
$cmd = 'cd ' . escapeshellarg($full_base) . ' && find . -path ' . $pattern . ' -type f 2>/dev/null | head -200';
$cmd = $this->isolation->wrapCommand($cmd);
$output = trim(shell_exec($cmd));
if(empty($output)){
$_pattern = isset($params['pattern']) ? $params['pattern'] : $pattern;
return array('output' => __('No files found matching pattern: $0', array($_pattern)), 'is_error' => false);
}
$files = explode("\n", $output);
$files = array_map(function($f){ return ltrim($f, './'); }, $files);
return array('output' => implode("\n", $files), 'is_error' => false);
}
private function tool_grep(array $params){
$pattern = isset($params['pattern']) ? $params['pattern'] : '';
$base_path = isset($params['path']) ? $params['path'] : '.';
$include = isset($params['include']) ? $params['include'] : '';
$max_results = isset($params['max_results']) ? intval($params['max_results']) : 50;
if(empty($pattern)){
return array('output' => __('Pattern is required'), 'is_error' => true);
}
if(!$this->isolation->isConfined()){
return array('output' => __('File search is disabled: no isolation mechanism available.'), 'is_error' => true);
}
$full_base = $this->file_manager->resolve_path($base_path);
if(strpos($full_base, $this->user_home_dir) !== 0){
return array('output' => __('Path outside allowed directory'), 'is_error' => true);
}
$cmd = 'cd ' . escapeshellarg($this->project_path) . ' && grep -rn --binary-files=without-match';
if(!empty($include)){
$cmd .= ' --include=' . escapeshellarg($include);
}
$cmd .= ' ' . escapeshellarg($pattern) . ' ' . escapeshellarg($full_base) . ' 2>/dev/null | head -' . $max_results;
$cmd = $this->isolation->wrapCommand($cmd);
$output = trim(shell_exec($cmd));
if(empty($output)){
return array('output' => __('No matches found for pattern: $0', array($pattern)), 'is_error' => false);
}
$output = preg_replace('/^' . preg_quote($this->project_path, '/') . '\//', '', $output);
return array('output' => $output, 'is_error' => false);
}
private function tool_list_directory(array $params){
$path = isset($params['path']) ? $params['path'] : '/';
$depth = isset($params['depth']) ? intval($params['depth']) : 2;
$result = $this->file_manager->list_directory($path, $depth);
if(!empty($result['error'])){
return array('output' => $result['error'], 'is_error' => true);
}
return array('output' => json_encode($result, JSON_PRETTY_PRINT), 'is_error' => false);
}
private function tool_web_fetch(array $params){
$url = isset($params['url']) ? $params['url'] : '';
if(empty($url)){
return array('output' => __('URL is required'), 'is_error' => true);
}
if(!filter_var($url, FILTER_VALIDATE_URL)){
return array('output' => __('Invalid URL'), 'is_error' => true);
}
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; Softaculous AI/1.0)'
));
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if($response === false){
return array('output' => __('Failed to fetch URL'), 'is_error' => true);
}
$response = strip_tags($response);
$response = preg_replace('/\s+/', ' ', $response);
$response = trim($response);
if(strlen($response) > 30000){
$response = substr($response, 0, 30000) . '... [truncated]';
}
return array('output' => $response, 'is_error' => false);
}
private function tool_todo_write(array $params){
$todos = isset($params['todos']) ? $params['todos'] : array();
return array('output' => __('Todo list updated with $0 items.', array(count($todos))), 'is_error' => false, 'todos' => $todos);
}
private function fuzzy_find($content, $search){
$search_trimmed = trim($search);
$search_lines = explode("\n", $search_trimmed);
if(count($search_lines) < 2) return null;
$max_file_lines = 5000;
$content_lines = explode("\n", $content);
if(count($content_lines) > $max_file_lines){
$content_lines = array_slice($content_lines, 0, $max_file_lines);
}
$first_line = trim($search_lines[0]);
$last_line = trim($search_lines[count($search_lines) - 1]);
$first_positions = array();
foreach($content_lines as $i => $line){
if(trim($line) === $first_line) $first_positions[] = $i;
}
foreach($first_positions as $start){
$end = $start + count($search_lines) - 1;
if($end >= count($content_lines)) continue;
if(trim($content_lines[$end]) === $last_line){
$match = implode("\n", array_slice($content_lines, $start, count($search_lines)));
return $match;
}
}
$max_similar_checks = 50;
$checks = 0;
foreach($content_lines as $i => $line){
if($checks >= $max_similar_checks) break;
if(strpos(trim($line), $first_line) !== false || strpos($first_line, trim($line)) !== false){
$end = min($i + count($search_lines), count($content_lines));
$candidate = implode("\n", array_slice($content_lines, $i, $end - $i));
if(strlen($candidate) > 100000) continue;
similar_text($search_trimmed, $candidate, $percent);
$checks++;
if($percent > 70) return $candidate;
}
}
return null;
}
private function make_mini_diff($old, $new){
$diff = "";
$old_lines = explode("\n", $old);
$new_lines = explode("\n", $new);
foreach($old_lines as $l) $diff .= "- " . $l . "\n";
foreach($new_lines as $l) $diff .= "+ " . $l . "\n";
return trim($diff);
}
private function compute_unified_diff($old, $new){
$old_lines = explode("\n", $old);
$new_lines = explode("\n", $new);
$max = max(count($old_lines), count($new_lines));
$diff = array();
$ctx = 3;
$buffer = array();
$in_change = false;
for($i = 0; $i < $max || !empty($buffer); $i++){
$o = isset($old_lines[$i]) ? $old_lines[$i] : null;
$n = isset($new_lines[$i]) ? $new_lines[$i] : null;
if($o !== null && $n !== null && $o === $n){
if($in_change){
$buffer[] = ' ' . $o;
if(count($buffer) > $ctx * 2 + 10){
$diff = array_merge($diff, array_slice($buffer, 0, count($buffer) - $ctx));
$buffer = array_slice($buffer, -$ctx);
$in_change = false;
}
}else{
$buffer[] = ' ' . $o;
if(count($buffer) > $ctx){
$buffer = array_slice($buffer, -$ctx);
}
}
}else{
if(!$in_change && !empty($buffer)){
$keep = min(count($buffer), $ctx);
$diff = array_merge($diff, array_slice($buffer, -$keep));
$buffer = array();
}
$in_change = true;
if($o !== null) $diff[] = '- ' . $o;
if($n !== null) $diff[] = '+ ' . $n;
}
}
if(!empty($buffer)){
$keep = min(count($buffer), $ctx);
$diff = array_merge($diff, array_slice($buffer, -$keep));
}
return implode("\n", $diff);
}
private function tool_file_download(array $params){
$url = isset($params['url']) ? $params['url'] : '';
$path = isset($params['path']) ? $params['path'] : '';
$overwrite = !empty($params['overwrite']);
if(empty($url) || empty($path)){
return array('output' => __('URL and path are required'), 'is_error' => true);
}
if(!filter_var($url, FILTER_VALIDATE_URL)){
return array('output' => __('Invalid URL: $0', array($url)), 'is_error' => true);
}
$scheme = parse_url($url, PHP_URL_SCHEME);
if(!in_array($scheme, array('http', 'https'))){
return array('output' => __('Only http/https URLs are allowed'), 'is_error' => true);
}
$resolved = $this->file_manager->resolve_path($path);
if(strpos($resolved, $this->project_path) !== 0){
return array('output' => __('Destination path is outside the project directory'), 'is_error' => true);
}
if(file_exists($resolved) && !$overwrite){
return array('output' => __('File already exists: $0. Set overwrite=true to replace.', array($path)), 'is_error' => true);
}
$dir = dirname($resolved);
if(!is_dir($dir)){
@mkdir($dir, 0755, true);
}
$fp = @fopen($resolved, 'w');
if(!$fp){
return array('output' => __('Cannot write to: $0', array($path)), 'is_error' => true);
}
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_FILE => $fp,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; Softaculous AI/1.0)',
CURLOPT_MAXFILESIZE => 52428800
));
$result = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$size = curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD);
$error = curl_error($ch);
curl_close($ch);
fclose($fp);
if(!$result){
@unlink($resolved);
return array('output' => __('Download failed: $0', array($error ?: __('Unknown error'))), 'is_error' => true);
}
if($http_code >= 400){
@unlink($resolved);
return array('output' => __('Download failed: HTTP $0', array($http_code)), 'is_error' => true);
}
$size_str = $this->format_bytes($size);
return array('output' => __('Downloaded $0 to $1 (HTTP $2)', array($size_str, $path, $http_code)), 'is_error' => false);
}
private function tool_archive(array $params){
$action = isset($params['action']) ? $params['action'] : '';
$source = isset($params['source']) ? $params['source'] : '';
$destination = isset($params['destination']) ? $params['destination'] : '';
if(empty($action) || empty($source)){
return array('output' => __('action and source are required'), 'is_error' => true);
}
if(!in_array($action, array('zip', 'unzip'))){
return array('output' => __('Action must be "zip" or "unzip"'), 'is_error' => true);
}
if($action === 'zip'){
return $this->archive_zip($source, $destination);
}else{
return $this->archive_unzip($source, $destination);
}
}
private function archive_zip($source, $destination){
$src_resolved = $this->file_manager->resolve_path($source);
if(strpos($src_resolved, $this->user_home_dir) !== 0){
return array('output' => __('Source path is outside the allowed directory'), 'is_error' => true);
}
if(!file_exists($src_resolved)){
return array('output' => __('Source not found: $0', array($source)), 'is_error' => true);
}
if(empty($destination)){
$destination = $source . '.zip';
}
if(substr($destination, -4) !== '.zip') $destination .= '.zip';
$dst_resolved = $this->file_manager->resolve_path($destination);
if(strpos($dst_resolved, $this->user_home_dir) !== 0){
return array('output' => __('Destination path is outside the allowed directory'), 'is_error' => true);
}
$zip = new ZipArchive();
if($zip->open($dst_resolved, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true){
return array('output' => __('Failed to create zip archive'), 'is_error' => true);
}
$count = 0;
if(is_dir($src_resolved)){
$base = basename($src_resolved);
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($src_resolved, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach($files as $file){
$local = $base . '/' . substr($file->getPathname(), strlen($src_resolved) + 1);
if($file->isDir()){
$zip->addEmptyDir($local);
}else{
if($file->getSize() > 52428800){
continue;
}
$zip->addFile($file->getPathname(), $local);
$count++;
}
}
}else{
$zip->addFile($src_resolved, basename($src_resolved));
$count = 1;
}
$zip->close();
$zip_size = $this->format_bytes(filesize($dst_resolved));
return array('output' => __('Created archive $0 ($1 files, $2)', array($destination, $count, $zip_size)), 'is_error' => false);
}
private function archive_unzip($source, $destination){
$src_resolved = $this->file_manager->resolve_path($source);
if(strpos($src_resolved, $this->user_home_dir) !== 0){
return array('output' => __('Source path is outside the allowed directory'), 'is_error' => true);
}
if(!file_exists($src_resolved)){
return array('output' => __('Source not found: $0', array($source)), 'is_error' => true);
}
if(empty($destination)){
$destination = dirname($source) ?: '.';
}
$dst_resolved = $this->file_manager->resolve_path($destination);
if(strpos($dst_resolved, $this->user_home_dir) !== 0){
return array('output' => __('Destination path is outside the allowed directory'), 'is_error' => true);
}
$zip = new ZipArchive();
if($zip->open($src_resolved) !== true){
return array('output' => __('Failed to open zip archive'), 'is_error' => true);
}
$count = $zip->numFiles;
$zip->extractTo($dst_resolved);
$zip->close();
return array('output' => __('Extracted $0 files to $1', array($count, $destination)), 'is_error' => false);
}
private function tool_search_replace(array $params){
$search = isset($params['search']) ? $params['search'] : '';
$replace = isset($params['replace']) ? $params['replace'] : '';
$include = isset($params['include']) ? $params['include'] : '';
$base_path = isset($params['path']) ? $params['path'] : '.';
$max_files = isset($params['max_files']) ? intval($params['max_files']) : 50;
$is_regex = !empty($params['regex']);
$max_files = max(1, min($max_files, 100));
if(empty($search)){
return array('output' => __('search pattern is required'), 'is_error' => true);
}
if(!$this->isolation->isConfined()){
return array('output' => __('Search and replace is disabled: no isolation mechanism available.'), 'is_error' => true);
}
$full_base = $this->file_manager->resolve_path($base_path);
if(strpos($full_base, $this->user_home_dir) !== 0){
return array('output' => __('Path outside allowed directory'), 'is_error' => true);
}
if($is_regex){
set_error_handler(function(){});
$test = preg_match($search, '');
restore_error_handler();
if($test === false){
return array('output' => __('Invalid regex pattern: $0', array($search)), 'is_error' => true);
}
}
$cmd = 'cd '.escapeshellarg($this->project_path).' && grep -rl --binary-files=without-match';
if(!empty($include)){
$cmd .= ' --include='.escapeshellarg($include);
}
$cmd .= ' '.escapeshellarg($search).' '.escapeshellarg($full_base).' 2>/dev/null | head -'.($max_files + 1);
$cmd = $this->isolation->wrapCommand($cmd);
$output = trim(shell_exec($cmd));
if(empty($output)){
return array('output' => __('No files found matching: $0', array($search)), 'is_error' => false);
}
$files = explode("\n", $output);
$truncated = false;
if(count($files) > $max_files){
$files = array_slice($files, 0, $max_files);
$truncated = true;
}
$results = array();
$total_replacements = 0;
foreach($files as $file){
$filepath = trim($file);
if(!file_exists($filepath) || !is_readable($filepath) || !is_writable($filepath)) continue;
$rel_path = str_replace($this->project_path . '/', '', $filepath);
$ctx_name = $this->is_context_file($rel_path);
if($ctx_name){
$content = $this->read_context_file($ctx_name);
}else{
$read_result = $this->file_manager->read_file($rel_path);
if(!empty($read_result['error'])) continue;
$content = $read_result['content'];
}
$count = 0;
if($is_regex){
$new_content = preg_replace($search, $replace, $content, -1, $count);
}else{
$new_content = str_replace($search, $replace, $content, $count);
}
if($count > 0 && $new_content !== null){
if($ctx_name){
$ok = $this->write_context_file($ctx_name, $new_content);
}else{
$write_result = $this->file_manager->write_file($rel_path, $new_content, false);
$ok = empty($write_result['error']);
}
if($ok){
$results[] = "{$rel_path}: {$count} replacement(s)";
$total_replacements += $count;
}
}
}
$output = __('Replaced "$0" in $1 file(s) ($2 total replacements)', array($search, count($results), $total_replacements))."\n";
$output .= implode("\n", $results);
if($truncated){
$output .= "\n".__('[Only processed first $0 files. Use max_files to increase.]', array($max_files));
}
return array('output' => $output, 'is_error' => false);
}
private function tool_php_eval(array $params){
$code = isset($params['code']) ? $params['code'] : '';
if(empty($code)){
return array('output' => __('code is required'), 'is_error' => true);
}
$forbidden = array('exec(', 'shell_exec(', 'system(', 'passthru(', 'popen(', 'proc_open(',
'pcntl_', 'putenv(', 'apache_', 'ini_set(', 'ini_restore(',
'mail(', 'header(', 'setcookie(', 'move_uploaded_file(',
'chmod(', 'chown(', 'chgrp(', 'unlink(', 'rmdir(',
'file_put_contents(', 'fwrite(', 'fputs(', 'rename(',
'eval(', 'assert(', 'preg_replace(', 'create_function(',
'call_user_func(', 'call_user_func_array(');
$code_lower = strtolower($code);
foreach($forbidden as $f){
if(strpos($code_lower, strtolower($f)) !== false){
return array('output' => __('Function/statement not allowed: $0', array($f)), 'is_error' => true);
}
}
if(!$this->isolation->isConfined()){
return array('output' => __('PHP execution is disabled: no isolation mechanism available.'), 'is_error' => true);
}
$tmp = tempnam(sys_get_temp_dir(), 'ai_php_');
$wrapped = "<?php\nchdir(".var_export($this->project_path, true).");\n";
if(file_exists($this->project_path.'/wp-load.php')){
$wrapped .= "define('ABSPATH', ".var_export($this->project_path.'/', true).");\n";
$wrapped .= "@include_once(".var_export($this->project_path.'/wp-load.php', true).");\n";
}
$wrapped .= "\n".$code."\n";
file_put_contents($tmp, $wrapped);
$env = array();
if(!empty($this->user_home_dir)){
$env['HOME'] = $this->user_home_dir;
}
$env['PATH'] = '/usr/local/bin:/usr/bin:/bin';
$open_basedir = $this->user_home_dir . ':' . sys_get_temp_dir();
$disable_functions = 'exec,shell_exec,system,passthru,popen,proc_open,pcntl_fork,pcntl_exec,putenv,ini_set,ini_restore,dl';
$php_cmd = sprintf('php -d open_basedir=%s -d disable_functions=%s %s',
escapeshellarg($open_basedir),
escapeshellarg($disable_functions),
escapeshellarg($tmp)
);
$full_cmd = $this->isolation->wrapPhpCommand($php_cmd);
$descriptors = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w')
);
$process = proc_open($full_cmd, $descriptors, $pipes, $this->project_path, $env);
if(!is_resource($process)){
@unlink($tmp);
return array('output' => __('Failed to execute PHP'), 'is_error' => true);
}
fclose($pipes[0]);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
$stdout = '';
$stderr = '';
$start = microtime(true);
$timeout = 10;
while(true){
if($this->should_abort()){
proc_terminate($process, 9);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
@unlink($tmp);
return array('output' => __('PHP execution aborted by user'), 'is_error' => true);
}
$status = proc_get_status($process);
if(!$status['running']){
while(($buf = fread($pipes[1], 8192)) !== '') $stdout .= $buf;
while(($buf = fread($pipes[2], 8192)) !== '') $stderr .= $buf;
break;
}
if(microtime(true) - $start >= $timeout){
proc_terminate($process, 9);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
@unlink($tmp);
return array('output' => __('PHP execution timed out after $0s', array($timeout)), 'is_error' => true);
}
$got_data = false;
while(($buf = fread($pipes[1], 8192)) !== ''){
$stdout .= $buf;
$got_data = true;
}
while(($buf = fread($pipes[2], 8192)) !== ''){
$stderr .= $buf;
$got_data = true;
}
if($got_data) $last_activity = microtime(true);
usleep(50000);
}
fclose($pipes[1]);
fclose($pipes[2]);
$return_code = proc_close($process);
@unlink($tmp);
$output = trim($stdout);
if(!empty($stderr)){
$output .= ($output ? "\n" : '') . trim($stderr);
}
if(strlen($output) > 20000){
$output = substr($output, 0, 20000) . "\n... [output truncated]";
}
if($return_code !== 0){
return array('output' => $output."\n".__('[Exit code: $0]', array($return_code)), 'is_error' => true);
}
return array('output' => $output ?: __('[PHP executed successfully with no output]'), 'is_error' => false);
}
/**
* Runs `php -l` (syntax-only check) on a PHP file in the project. The lint
* command never executes the file, so it is safe to run even when no shell
* isolation is available. Lets the AI verify PHP files before declaring a
* task complete.
*/
private function tool_php_lint(array $params){
$path = isset($params['path']) ? $params['path'] : '';
if(empty($path)){
return array('output' => __('Path is required'), 'is_error' => true);
}
$resolved = $this->file_manager->resolve_path($path);
if(empty($resolved) || strpos($resolved, $this->user_home_dir) !== 0){
return array('output' => __('Path outside allowed directory'), 'is_error' => true);
}
if(!is_file($resolved)){
return array('output' => __('File not found: $0', array($path)), 'is_error' => true);
}
if(substr($resolved, -4) !== '.php'){
return array('output' => __('Not a PHP file: $0', array($path)), 'is_error' => true);
}
if(!$this->isolation->isConfined()){
return array('output' => __('PHP lint is disabled: no isolation mechanism available.'), 'is_error' => true);
}
$cmd = 'php -l ' . escapeshellarg($resolved) . ' 2>&1';
// Route through the isolation layer (jail/cagefs) for consistency,
// even though php -l is syntax-only and does not execute the file.
$cmd = $this->isolation->wrapCommand($cmd);
$output = trim((string)@shell_exec($cmd));
if(empty($output)){
return array('output' => __('Failed to run the PHP linter (php CLI not available?)'), 'is_error' => true);
}
$ok = (strpos($output, 'No syntax errors detected') !== false);
return array('output' => $output, 'is_error' => !$ok);
}
private function run_process($cmd, $timeout, $env){
$descriptors = array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'w')
);
$process = proc_open($cmd, $descriptors, $pipes, null, $env);
if(!is_resource($process)){
return array('output' => __('Failed to execute command'), 'is_error' => true);
}
fclose($pipes[0]);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
$stdout = '';
$stderr = '';
$start = microtime(true);
$last_activity = $start;
$max_idle_time = 120;
while(true){
if($this->should_abort()){
proc_terminate($process, 9);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
return array('output' => __('Command aborted by user'), 'is_error' => true);
}
$status = proc_get_status($process);
if(!$status['running']){
while(($buf = fread($pipes[1], 8192)) !== '') $stdout .= $buf;
while(($buf = fread($pipes[2], 8192)) !== '') $stderr .= $buf;
break;
}
$elapsed = microtime(true) - $start;
if($elapsed >= $timeout){
proc_terminate($process, 9);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
$output = $stdout . ($stdout && $stderr ? "\n" : '') . $stderr;
return array('output' => __('Command timed out after $0s', array($timeout))."\n{$output}", 'is_error' => true);
}
$got_data = false;
while(($buf = fread($pipes[1], 8192)) !== ''){
$stdout .= $buf;
$got_data = true;
}
while(($buf = fread($pipes[2], 8192)) !== ''){
$stderr .= $buf;
$got_data = true;
}
if($got_data){
$last_activity = microtime(true);
}elseif((microtime(true) - $last_activity) > $max_idle_time){
proc_terminate($process, 9);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
$output = $stdout . ($stdout && $stderr ? "\n" : '') . $stderr;
return array('output' => __('Command idle timeout after $0s with no output', array($max_idle_time))."\n{$output}", 'is_error' => true);
}
usleep(50000);
}
fclose($pipes[1]);
fclose($pipes[2]);
$return_code = proc_close($process);
$output = '';
if(!empty($stdout)) $output .= $stdout;
if(!empty($stderr)) $output .= ($output ? "\n" : '') . $stderr;
if(strlen($output) > 30000){
$output = mb_substr($output, 0, 30000) . "\n... [output truncated]";
}
if($return_code !== 0){
$output .= "\n".__('[Exit code: $0]', array($return_code));
return array('output' => $output, 'is_error' => true);
}
return array('output' => $output ? $output : __('[Command completed successfully with no output]'), 'is_error' => false);
}
private function tool_apply_patch(array $params){
$path = isset($params['path']) ? $params['path'] : '';
$patch = isset($params['patch']) ? $params['patch'] : '';
if(empty($path)){
return array('output' => __('Path is required'), 'is_error' => true);
}
if(empty($patch)){
return array('output' => __('Patch content is required'), 'is_error' => true);
}
// For context files, read from the persistent context dir
$ctx_name = $this->is_context_file($path);
if($ctx_name){
$original = $this->read_context_file($ctx_name);
if($original === ''){
return array('output' => __('File not found'), 'is_error' => true);
}
}else{
$result = $this->file_manager->read_file($path);
if(!empty($result['error'])){
return array('output' => $result['error'], 'is_error' => true);
}
$original = $result['content'];
}
$patched = $this->apply_unified_patch($original, $patch);
if($patched === false){
return array('output' => __('Failed to apply patch. The patch may not match the file content.'), 'is_error' => true);
}
// Context files live in the persistent per-user context directory, so
// persist the patch there instead of the project root.
if($ctx_name){
if(!$this->write_context_file($ctx_name, $patched)){
return array('output' => __('Failed to save context file'), 'is_error' => true);
}
}else{
$write_result = $this->file_manager->write_file($path, $patched, false);
if(!empty($write_result['error'])){
return array('output' => $write_result['error'], 'is_error' => true);
}
}
$lines = substr_count($patched, "\n") + 1;
$display_path = $ctx_name ? $ctx_name : $path;
$return = array('output' => __('Successfully applied patch to $0 ($1 lines)', array($display_path, $lines)), 'is_error' => false);
if($original !== $patched){
$return['diff'] = $this->compute_unified_diff($original, $patched);
}
return $return;
}
private function apply_unified_patch($content, $patch){
$lines = explode("\n", $content);
$patch_lines = explode("\n", $patch);
$result = $lines;
$offset = 0;
$i = 0;
$patch_lines_count = count($patch_lines);
while($i < $patch_lines_count){
$line = $patch_lines[$i];
if(preg_match('/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/', $line, $m)){
$old_start = intval($m[1]);
$old_count = isset($m[2]) ? intval($m[2]) : 1;
$new_start = intval($m[3]);
$new_count = isset($m[4]) ? intval($m[4]) : 1;
$i++;
$old_lines = array();
$new_lines = array();
while($i < $patch_lines_count){
$pl = $patch_lines[$i];
if(strlen($pl) === 0 && $i === $patch_lines_count - 1) break;
if(preg_match('/^@@\s+/', $pl)) break;
$first_char = strlen($pl) > 0 ? $pl[0] : ' ';
$rest = strlen($pl) > 0 ? substr($pl, 1) : '';
if($first_char === '-'){
$old_lines[] = $rest;
}elseif($first_char === '+'){
$new_lines[] = $rest;
}elseif($first_char === ' ' || $first_char === ''){
$old_lines[] = $rest;
$new_lines[] = $rest;
}elseif($first_char === '\\'){
// "\ No newline at end of file" - skip
}else{
break;
}
$i++;
}
$found = false;
$search_start = max(0, $old_start - 1 - 5 + $offset);
$search_end = min(count($result), $old_start - 1 + 5 + $offset);
for($s = $search_start; $s <= $search_end; $s++){
$match = true;
for($j = 0; $j < count($old_lines); $j++){
if($s + $j >= count($result) || rtrim($result[$s + $j]) !== rtrim($old_lines[$j])){
$match = false;
break;
}
}
if($match){
array_splice($result, $s, count($old_lines), $new_lines);
$offset += count($new_lines) - count($old_lines);
$found = true;
break;
}
}
if(!$found){
for($s = 0; $s < count($result); $s++){
$match = true;
for($j = 0; $j < count($old_lines); $j++){
if($s + $j >= count($result) || rtrim($result[$s + $j]) !== rtrim($old_lines[$j])){
$match = false;
break;
}
}
if($match){
array_splice($result, $s, count($old_lines), $new_lines);
$offset += count($new_lines) - count($old_lines);
$found = true;
break;
}
}
if(!$found){
return false;
}
}
}else{
$i++;
}
}
return implode("\n", $result);
}
private function tool_question(array $params){
$question = isset($params['question']) ? $params['question'] : '';
$options = isset($params['options']) ? $params['options'] : array();
$multiple = !empty($params['multiple']);
$allow_custom = isset($params['custom']) ? $params['custom'] : true;
if(empty($question)){
return array('output' => __('Question text is required'), 'is_error' => true);
}
$q_data = array(
'question' => $question,
'options' => $options,
'multiple' => $multiple,
'custom' => $allow_custom
);
return array(
'output' => json_encode($q_data),
'is_error' => false,
'_question' => true
);
}
private function format_bytes($bytes){
if($bytes >= 1048576) return round($bytes / 1048576, 1).'MB';
if($bytes >= 1024) return round($bytes / 1024, 1).'KB';
return $bytes.'B';
}
private function is_dangerous_command($command){
$protected_dirs = array('.softaculous', '.ssh', '.gnupg', '.softaculous-pro', 'softaculous-pro');
foreach($protected_dirs as $dir){
if(preg_match('#/(?:' . preg_quote($dir, '#') . ')(?:/|$)#i', $command)){
if(preg_match('/\b(rm|rmdir|mv|chmod|chown|chgrp|ln|unlink)\b/', $command)){
return true;
}
}
}
$dangerous = array(
'/\brm\s+-[a-zA-Z]*r[a-zA-Z]*f\s+\/\s*$/',
'/\brm\s+-[a-zA-Z]*f[a-zA-Z]*r\s+\/\s*$/',
'/\brm\s+-rf\s+~\s*$/',
'/\brm\s+-fr\s+~\s*$/',
'/:\(\)\{.*;\}\s*;&/',
'/\bdd\s+if=/',
'/\bmkfs\b/',
'/\bformat\s+[a-z]:/i',
'/\bshutdown\b/',
'/\breboot\b/',
'/\bhalt\b/',
'/\binit\s+[06]\b/',
'/>\s*\/dev\/sda/',
'/\bchmod\s+-R\s+777\s+\//',
'/\bchown\s+-R\s+.*\s+\//',
'/\bcurl\b.*\|\s*(ba)?sh/',
'/\bwget\b.*\|\s*(ba)?sh/',
'/\bsudo\b/',
'/\bsu\s+/',
'/\bssh\b/',
'/\bscp\b/',
'/\brsync\b/',
'/\bnc\s/',
'/\bncat\b/',
'/\bsocat\b/',
'/\bcrontab\b/',
'/\bat\s+[a-z]+\s/',
'/\bsystemctl\b/',
'/\bservice\b/',
'/\biptables\b/',
'/\bip\s+route\b/',
'/\bmysql\s/',
'/\bpsql\s/',
'/\bmongosh\b/',
'/\bpython\d?\s+-c\s/',
'/\bperl\s+-e\s/',
'/\bruby\s+-e\s/',
'/\bkill\s+-9\s+1\b/',
'/\bkillall\b/',
'/\bpkill\b/',
'/\bshutdown\b/',
'/\bpoweroff\b/',
);
foreach($dangerous as $pattern){
if(preg_match($pattern, $command)) return true;
}
return false;
}
}Back to Directory