$value) { $excludes = $value['exclude']; $excludePaths = []; foreach ($excludes as $exclude) { if ('setup' == $exclude['type']) { $excludePaths[] = BP . '/setup/' . $exclude['path']; } else { $excludePaths[] = $componentRegistrar->getPath($exclude['type'], $exclude['name']) . '/' . $exclude['path']; } } $data[$key]['exclude'] = $excludePaths; } } /** * Isolate including a file into a method to reduce scope * * @param string $file * @return array */ private static function readList($file) { return include $file; } /** * Detect unsecure functions usage for changed files in whitelist with the exception of blacklist * * @return void */ public function testUnsecureFunctionsUsage() { $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); $functionDetector = new FunctionDetector(); $invoker( function ($fileFullPath) use ($functionDetector) { $functions = $this->getFunctions($fileFullPath); $lines = $functionDetector->detect($fileFullPath, array_keys($functions)); $message = ''; if (!empty($lines)) { $message = $this->composeMessage($fileFullPath, $lines, $functions); } $this->assertEmpty( $lines, $message ); }, $this->getFilesToVerify() ); } /** * Compose message * * @param string $fileFullPath * @param array $lines * @param array $functionRules * @return string */ private function composeMessage($fileFullPath, $lines, $functionRules) { $result = ''; foreach ($lines as $lineNumber => $detectedFunctions) { $detectedFunctionRules = array_intersect_key($functionRules, array_flip($detectedFunctions)); $replacementString = ''; foreach ($detectedFunctionRules as $function => $functionRule) { $replacement = $functionRule['replacement']; if (is_array($replacement)) { $replacement = array_unique($replacement); $replacement = count($replacement) > 1 ? "[\n\t\t\t" . implode("\n\t\t\t", $replacement) . "\n\t\t]" : $replacement[0]; } $replacement = empty($replacement) ? 'No suggested replacement at this time' : $replacement; $replacementString .= "\t\t'$function' => '$replacement'\n"; } $result .= sprintf( "Functions '%s' are not secure in %s. \n\tSuggested replacement:\n%s", implode(', ', $detectedFunctions), $fileFullPath . ':' . $lineNumber, $replacementString ); } return $result; } /** * Get files to be verified * * @return array */ private function getFilesToVerify() { $fileExtensions = $this->fileExtensions; $directoriesToScan = Files::init()->readLists(__DIR__ . '/_files/security/whitelist.txt'); $filesToVerify = []; foreach (glob(__DIR__ . '/../_files/changed_files*') as $listFile) { $filesToVerify = array_merge( $filesToVerify, file($listFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ); } array_walk( $filesToVerify, function (&$file) { $file = [BP . '/' . $file]; } ); $filesToVerify = array_filter( $filesToVerify, function ($path) use ($directoriesToScan, $fileExtensions) { if (!file_exists($path[0])) { return false; } $path = realpath($path[0]); foreach ($directoriesToScan as $directory) { $directory = realpath($directory); if (strpos($path, $directory) === 0) { if (preg_match($fileExtensions, $path)) { // skip unit tests if (preg_match('#' . preg_quote('Test/Unit', '#') . '#', $path)) { return false; } return true; } } } return false; } ); return $filesToVerify; } /** * Get functions for the given file * * @param string $fileFullPath * @return array */ private function getFunctions($fileFullPath) { $fileExtension = pathinfo($fileFullPath, PATHINFO_EXTENSION); $functions = []; if ($fileExtension == 'php') { $functions = self::$phpUnsecureFunctions; } elseif ($fileExtension == 'js') { $functions = self::$jsUnsecureFunctions; } elseif ($fileExtension == 'phtml') { $functions = array_merge_recursive(self::$phpUnsecureFunctions, self::$jsUnsecureFunctions); } foreach ($functions as $function => $functionRules) { if (in_array($fileFullPath, $functionRules['exclude'])) { unset($functions[$function]); } } return $functions; } }