diff --git a/.github/workflows/veracode.yml b/.github/workflows/veracode.yml new file mode 100644 index 0000000000..ac809aba71 --- /dev/null +++ b/.github/workflows/veracode.yml @@ -0,0 +1,28 @@ +name: Veracode DAST Essentials + +on: + push: + branches: + - develop + workflow_dispatch: + workflow_call: + +jobs: + veracode_security_scan: + runs-on: ubuntu-latest + name: Run Veracode DAST Essentials Scan + steps: + - name: Veracode Action Step + id: Veracode + uses: veracode/veracode-dast-essentials-action@v1.0.2 + with: + VERACODE_WEBHOOK: '${{ secrets.VERACODE_WEBHOOK }}' + VERACODE_SECRET_ID: '${{ secrets.VERACODE_SECRET_ID }}' + VERACODE_SECRET_ID_KEY: '${{ secrets.VERACODE_SECRET_ID_KEY }}' + REGION: 'us' + pull-report: 'true' + - name: Publish Test Report + uses: mikepenz/action-junit-report@v1 + with: + report_paths: 'report.xml' + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/ProcessMaker/Cache/AbstractCacheFactory.php b/ProcessMaker/Cache/AbstractCacheFactory.php index 7ca46776ee..b3779f3f8b 100644 --- a/ProcessMaker/Cache/AbstractCacheFactory.php +++ b/ProcessMaker/Cache/AbstractCacheFactory.php @@ -10,6 +10,7 @@ abstract class AbstractCacheFactory implements CacheFactoryInterface { protected static ?CacheInterface $testInstance = null; + protected static bool $storeMetrics = true; /** * Set a test instance for the factory @@ -38,7 +39,11 @@ public static function create(CacheManager $cacheManager, CacheMetricsInterface $cache = static::createInstance($cacheManager); // Wrap with metrics decorator - return new CacheMetricsDecorator($cache, $metrics); + if (static::$storeMetrics) { + return new CacheMetricsDecorator($cache, $metrics); + } + + return $cache; } /** diff --git a/ProcessMaker/Cache/Settings/SettingCacheFactory.php b/ProcessMaker/Cache/Settings/SettingCacheFactory.php index fac9818c41..6145c49c0e 100644 --- a/ProcessMaker/Cache/Settings/SettingCacheFactory.php +++ b/ProcessMaker/Cache/Settings/SettingCacheFactory.php @@ -8,6 +8,8 @@ class SettingCacheFactory extends AbstractCacheFactory { + protected static bool $storeMetrics = false; + /** * Create the specific settings cache instance * diff --git a/ProcessMaker/Console/Commands/CacheScreensClear.php b/ProcessMaker/Console/Commands/CacheScreensClear.php new file mode 100644 index 0000000000..e6bd54cb5f --- /dev/null +++ b/ProcessMaker/Console/Commands/CacheScreensClear.php @@ -0,0 +1,33 @@ +clearCompiledAssets(); + $this->info('Cache for screens cleared'); + } +} diff --git a/ProcessMaker/Console/Kernel.php b/ProcessMaker/Console/Kernel.php index 30ac716641..1df94ee2fc 100644 --- a/ProcessMaker/Console/Kernel.php +++ b/ProcessMaker/Console/Kernel.php @@ -44,6 +44,37 @@ protected function schedule(Schedule $schedule) $schedule->command('cache:metrics --format=json > storage/logs/processmaker-cache-metrics.json') ->daily(); + + $clearInterval = config('metrics.clear_interval', 10); + switch ((int) $clearInterval) { + case 1: + $schedule->command('metrics:clear')->everyMinute(); + break; + case 2: + $schedule->command('metrics:clear')->everyTwoMinutes(); + break; + case 5: + $schedule->command('metrics:clear')->everyFiveMinutes(); + break; + case 10: + $schedule->command('metrics:clear')->everyTenMinutes(); + break; + case 15: + $schedule->command('metrics:clear')->everyFifteenMinutes(); + break; + case 30: + $schedule->command('metrics:clear')->everyThirtyMinutes(); + break; + case 60: + $schedule->command('metrics:clear')->hourly(); + break; + case 1440: + $schedule->command('metrics:clear')->daily(); + break; + default: + $schedule->command('metrics:clear')->cron("*/{$clearInterval} * * * *"); + break; + } } /** diff --git a/ProcessMaker/Console/Migration/ExtendedMigrateCommand.php b/ProcessMaker/Console/Migration/ExtendedMigrateCommand.php index 9272209c5d..0b577b4b71 100644 --- a/ProcessMaker/Console/Migration/ExtendedMigrateCommand.php +++ b/ProcessMaker/Console/Migration/ExtendedMigrateCommand.php @@ -4,6 +4,8 @@ use Illuminate\Database\Console\Migrations\MigrateCommand as BaseMigrateCommand; use Illuminate\Support\Facades\Cache; +use ProcessMaker\Cache\Screens\ScreenCacheFactory; +use ProcessMaker\Cache\Settings\SettingCacheFactory; use ProcessMaker\Helpers\CachedSchema; use ProcessMaker\Models\ProcessMakerModel; @@ -13,6 +15,8 @@ public function handle(): void { Cache::tags(ProcessMakerModel::MIGRATION_COLUMNS_CACHE_KEY)->flush(); Cache::tags(CachedSchema::CACHE_TAG)->flush(); + SettingCacheFactory::getSettingsCache()->clear(); + ScreenCacheFactory::getScreenCache()->clearCompiledAssets(); parent::handle(); } diff --git a/ProcessMaker/Events/MessageEventThrown.php b/ProcessMaker/Events/MessageEventThrown.php index 7416deb04a..93b3a60611 100644 --- a/ProcessMaker/Events/MessageEventThrown.php +++ b/ProcessMaker/Events/MessageEventThrown.php @@ -70,4 +70,19 @@ public function broadcastWith(): array 'data' => (object) $this->message->getData($this->processRequest), ]; } + + public function getProcessRequest() + { + return $this->processRequest; + } + + public function getNode() + { + return $this->node; + } + + public function getMessage() + { + return $this->message; + } } diff --git a/ProcessMaker/Events/ProcessUpdated.php b/ProcessMaker/Events/ProcessUpdated.php index e1bf14f692..01d253303b 100644 --- a/ProcessMaker/Events/ProcessUpdated.php +++ b/ProcessMaker/Events/ProcessUpdated.php @@ -24,6 +24,8 @@ class ProcessUpdated implements ShouldBroadcastNow private $processRequest; + public $activeTokens; + /** * Create a new event instance. * @@ -35,7 +37,7 @@ public function __construct(ProcessRequest $processRequest, $event, TokenInterfa $this->event = $event; $this->processRequest = $processRequest; - + $this->activeTokens = $this->processRequest->getActiveTokens($this->processRequest); if ($token) { $this->tokenId = $token->getId(); $this->elementType = $token->element_type; @@ -65,7 +67,7 @@ public function broadcastOn() /** * Return the process request. * - * @return \ProcessMaker\Models\ProcessRequest + * @return ProcessRequest */ public function getProcessRequest() { diff --git a/ProcessMaker/Filters/BaseFilter.php b/ProcessMaker/Filters/BaseFilter.php index 66b245d06a..247cb770d4 100644 --- a/ProcessMaker/Filters/BaseFilter.php +++ b/ProcessMaker/Filters/BaseFilter.php @@ -29,6 +29,8 @@ abstract class BaseFilter public const TYPE_PROCESS_NAME = 'ProcessName'; + public const PROCESS_NAME_IN_REQUEST = 'process_request.name'; + public const TYPE_RELATIONSHIP = 'Relationship'; public string|null $subjectValue; @@ -98,6 +100,11 @@ private function apply($query): void $this->valueAliasAdapter($valueAliasMethod, $query); } elseif ($this->subjectType === self::TYPE_PROCESS) { $this->filterByProcessId($query); + } elseif ($this->subjectValue === self::PROCESS_NAME_IN_REQUEST) { + // For performance reasons, the task list uses the column process_request.name + // But the filters must use the Process table, for this reason the subjectType is updated + $this->subjectType = self::TYPE_PROCESS_NAME; + $this->filterByProcessName($query); } elseif ($this->subjectType === self::TYPE_PROCESS_NAME) { $this->filterByProcessName($query); } elseif ($this->subjectType === self::TYPE_RELATIONSHIP) { diff --git a/ProcessMaker/Helpers/DefaultColumns.php b/ProcessMaker/Helpers/DefaultColumns.php index d197553691..f3c5ca1e72 100644 --- a/ProcessMaker/Helpers/DefaultColumns.php +++ b/ProcessMaker/Helpers/DefaultColumns.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Auth; use ProcessMaker\Package\SavedSearch\Http\Controllers\SavedSearchController; use ProcessMaker\Package\SavedSearch\Models\SavedSearch; +use ProcessMaker\Package\SavedSearch\Traits\HasDataColumns; class DefaultColumns { @@ -40,4 +41,51 @@ public static function get(string $key) return $defaultColumns; } + + /** + * Simple verification if columns are default - returns true or false + * + * @param array|null $savedColumns The columns to verify + * @param string $key The key to identify the type can be tasks or requests + * @return bool True if columns have the same fields as defaults, false otherwise + */ + public static function verifyDefaultColumns($savedColumns, string $key): bool + { + // Handle null savedColumns + if ($savedColumns === null || !is_array($savedColumns) || empty($savedColumns)) { + return true; + } + + // Determine the type based on the key + $type = null; + if ($key === SavedSearch::KEY_TASKS) { + $type = 'task'; + } else { + return true; + } + + // Get default columns using the HasDataColumns trait + $defaultColumns = SavedSearch::getDefaultColumns($type); + if ($defaultColumns->isEmpty()) { + return true; + } + + // Extract only the 'field' values from default columns + $defaultFields = $defaultColumns->map(function ($column) { + return $column->field ?? null; + })->filter()->sort()->values()->toArray(); + + // Extract only the 'field' values from saved columns (handling both formats) + $savedFields = collect($savedColumns)->map(function ($column) { + // Handle both object and array formats + if (is_object($column)) { + return $column->field ?? null; + } else { + return $column['field'] ?? null; + } + })->filter()->sort()->values()->toArray(); + + // Compare only the field arrays + return $defaultFields == $savedFields; + } } diff --git a/ProcessMaker/Http/Controllers/Admin/UserController.php b/ProcessMaker/Http/Controllers/Admin/UserController.php index e0fdd8623e..3db3fc690b 100644 --- a/ProcessMaker/Http/Controllers/Admin/UserController.php +++ b/ProcessMaker/Http/Controllers/Admin/UserController.php @@ -3,6 +3,7 @@ namespace ProcessMaker\Http\Controllers\Admin; use Illuminate\Contracts\View\Factory; +use Illuminate\Support\Facades\Auth; use Illuminate\View\View; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Models\Group; @@ -87,6 +88,9 @@ function ($result, $item) { $addons = $this->getPluginAddons('edit', compact(['user'])); $addonsSettings = $this->getPluginAddons('edit.settings', compact(['user'])); + // The user can only create tokens for themselves or if they are an administrator. + $canCreateTokens = Auth::user()->is_administrator || Auth::user()->id === $user->id; + return view('admin.users.edit', compact( 'user', 'groups', @@ -104,6 +108,7 @@ function ($result, $item) { 'addons', 'addonsSettings', 'ssoUser', + 'canCreateTokens', )); } diff --git a/ProcessMaker/Http/Controllers/Api/DevLinkController.php b/ProcessMaker/Http/Controllers/Api/DevLinkController.php index 7b23ec3032..a5f0c63e31 100644 --- a/ProcessMaker/Http/Controllers/Api/DevLinkController.php +++ b/ProcessMaker/Http/Controllers/Api/DevLinkController.php @@ -283,7 +283,12 @@ public function addAsset(Request $request, Bundle $bundle) public function addSettings(Request $request, Bundle $bundle) { - $bundle->addSettings($request->input('setting'), $request->input('config'), $request->input('type')); + $bundle->addSettings( + $request->input('setting'), + $request->input('config'), + $request->input('type'), + $request->input('replaceIds', false) + ); } public function addAssetToBundles(Request $request) diff --git a/ProcessMaker/Http/Controllers/Api/PermissionController.php b/ProcessMaker/Http/Controllers/Api/PermissionController.php index f5636d846d..093cf7dcc6 100644 --- a/ProcessMaker/Http/Controllers/Api/PermissionController.php +++ b/ProcessMaker/Http/Controllers/Api/PermissionController.php @@ -3,6 +3,7 @@ namespace ProcessMaker\Http\Controllers\Api; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use ProcessMaker\Events\PermissionChanged; use ProcessMaker\Events\PermissionUpdated; @@ -87,9 +88,15 @@ public function update(Request $request) $entity = User::findOrFail($request->input('user_id')); // Obtain user old Permissions before save $originalPermissionNames = $entity->permissions()->pluck('name')->toArray(); - if ($request->has('is_administrator')) { - $entity->is_administrator = filter_var($request->input('is_administrator'), FILTER_VALIDATE_BOOLEAN); + $isSettingToAdmin = $request->boolean('is_administrator'); + + if ((!Auth::user()->is_administrator && $entity->is_administrator) || + ($isSettingToAdmin && !Auth::user()->is_administrator)) { + return response()->json(['message' => 'You are not authorized to modify administrator privileges'], 403); + } + + $entity->is_administrator = $isSettingToAdmin; $entity->save(); } } elseif ($request->input('group_id')) { diff --git a/ProcessMaker/Http/Controllers/Api/ProcessController.php b/ProcessMaker/Http/Controllers/Api/ProcessController.php index 1b9e6dc39c..c0b41f696f 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessController.php @@ -386,6 +386,11 @@ public function store(Request $request) $processCreated = ProcessCreated::BPMN_CREATION; } + // Replace html entities with the correct characters + if (isset($data['bpmn'])) { + $data['bpmn'] = BpmnDocument::replaceHtmlEntities($data['bpmn']); + } + if ($schemaErrors = $this->validateBpmn($request)) { return response( ['message' => __('The bpm definition is not valid'), @@ -483,6 +488,11 @@ public function update(Request $request, Process $process) $request->validate($rules); $original = $process->getOriginal(); + // Replace html entities with the correct characters + if ($request->has('bpmn')) { + $request->merge(['bpmn' => BpmnDocument::replaceHtmlEntities($request->input('bpmn'))]); + } + // bpmn validation if ($schemaErrors = $this->validateBpmn($request)) { $warnings = []; @@ -600,7 +610,8 @@ public function updateBpmn(Request $request, Process $process) $process->alternative = $request->input('alternative'); } - $process->bpmn = $request->input('bpmn'); + $bpmn = BpmnDocument::replaceHtmlEntities($request->input('bpmn')); + $process->bpmn = $bpmn; $process->name = $request->input('name'); $process->description = $request->input('description'); $process->saveOrFail(); diff --git a/ProcessMaker/Http/Controllers/Api/SettingController.php b/ProcessMaker/Http/Controllers/Api/SettingController.php index de90e2a461..8bee58deac 100644 --- a/ProcessMaker/Http/Controllers/Api/SettingController.php +++ b/ProcessMaker/Http/Controllers/Api/SettingController.php @@ -3,6 +3,7 @@ namespace ProcessMaker\Http\Controllers\Api; use DB; +use Illuminate\Database\QueryException; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage; @@ -239,6 +240,7 @@ public function index(Request $request) */ public function update(Setting $setting, Request $request) { + $this->validateWhiteListURL($request); $request->validate(Setting::rules($setting, $request->input('key') == 'users.properties'), Setting::messages()); $setting->config = $request->input('config'); $original = array_intersect_key($setting->getOriginal(), $setting->getDirty()); @@ -261,10 +263,53 @@ public function update(Setting $setting, Request $request) ]); // set to cache with key and setting $settingCache->set($key, $setting->refresh()); + // update config + config([$setting->key => $setting->config]); return response([], 204); } + public function store(Request $request) + { + $setting = new Setting(); + + try { + $this->validateWhiteListURL($request); + $setting->fill($request->json()->all()); + $setting->saveOrFail(); + + return response([], 201); + } catch (QueryException $e) { + // Check for duplicate entry error code + if ($e->errorInfo[1] == 1062) { + return response()->json([ + 'message' => 'The "Site Name" you\'re trying to add already exists. Please select a different name or review the existing entries to avoid duplication.', + ], 409); + } + + // Handle other query exceptions + return response()->json(['message' => 'An error occurred while saving the setting.'], 500); + } + } + + /** + * Validate the white list URL + * + * @param Request $request + * @return void + */ + private function validateWhiteListURL(Request $request) + { + if (strpos($request->input('key'), 'white_list.') === 0 || strpos($request->input('key'), 'white_list_frame.') === 0) { + $request->validate([ + 'config' => [ + 'required', + validateURL(), + ], + ]); + } + } + public function destroy(Setting $setting) { $setting->delete(); diff --git a/ProcessMaker/Http/Controllers/Api/TaskController.php b/ProcessMaker/Http/Controllers/Api/TaskController.php index 789c16b30f..83d04408df 100644 --- a/ProcessMaker/Http/Controllers/Api/TaskController.php +++ b/ProcessMaker/Http/Controllers/Api/TaskController.php @@ -327,7 +327,7 @@ public function update(Request $request, ProcessRequestToken $task) return abort(422, __('Task already closed')); } // Skip ConvertEmptyStringsToNull and TrimStrings middlewares - $data = json_decode($request->getContent(), true); + $data = json_optimize_decode($request->getContent(), true); $data = SanitizeHelper::sanitizeData($data['data'], null, $task->processRequest->do_not_sanitize ?? []); //Call the manager to trigger the start event $process = $task->process; diff --git a/ProcessMaker/Http/Controllers/Api/UserController.php b/ProcessMaker/Http/Controllers/Api/UserController.php index b26e26b2a1..658aef5b4b 100644 --- a/ProcessMaker/Http/Controllers/Api/UserController.php +++ b/ProcessMaker/Http/Controllers/Api/UserController.php @@ -477,8 +477,19 @@ public function update(User $user, Request $request) //Call new Event to store User Changes into LOG UserUpdated::dispatch($user, $changes, $original); - if ($request->has('avatar')) { - $this->uploadAvatar($user, $request); + try { + if ($request->has('avatar')) { + $this->uploadAvatar($user, $request); + } + } catch (\Exception $e) { + return response([ + 'message' => 'Error uploading avatar', + 'errors' => [ + 'avatar' => [ + $e->getMessage(), + ], + ], + ], 422); } RecommendationEngine::handleUserSettingChanges($user, $original); @@ -735,12 +746,34 @@ private function uploadAvatar(User $user, Request $request) throw new \Exception('invalid image type'); } + // Validate base64 string + if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $data)) { + throw new \Exception('invalid base64 string'); + } + $data = base64_decode($data); if ($data === false) { throw new \Exception('base64_decode failed'); } + // Validate image content + if ($type === 'svg') { + // For SVG files, validate against XSS + if (preg_match('/ diff --git a/resources/js/admin/settings/components/SettingText.vue b/resources/js/admin/settings/components/SettingText.vue index 11c81d4bda..d4e7e549bd 100644 --- a/resources/js/admin/settings/components/SettingText.vue +++ b/resources/js/admin/settings/components/SettingText.vue @@ -21,11 +21,8 @@ @hidden="onModalHidden" @shown="onModalShown" > -