<?php
/**
 * Proven PHP data-flow detector.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Detection\Php;

use LocalGuardScanner\Detection\Rules\Rule;
use LocalGuardScanner\Detection\Rules\RuleSet;
use LocalGuardScanner\Domain\Detection\Evidence;
use LocalGuardScanner\Domain\Detection\MalwareHeuristicDetector;
use LocalGuardScanner\Domain\Detection\ScannableObject;

/**
 * Maps bounded flow analysis signals to signed evidence definitions.
 */
final class PhpDataFlowDetector implements MalwareHeuristicDetector {
	/** @var PhpDataFlowAnalyzer */
	private $analyzer;
	/** @var Rule[] */
	private $rules;

	/** @param PhpDataFlowAnalyzer $analyzer Analyzer. @param RuleSet $rules Signed rules. */
	public function __construct( PhpDataFlowAnalyzer $analyzer, RuleSet $rules ) {
		$this->analyzer = $analyzer;
		$this->rules    = $rules->of_type( 'token_sequence' );
	}

	/** @return string */
	public function id() {
		return 'php.data_flow';
	}

	/** @inheritDoc */
	public function inspect( ScannableObject $scanned_object ) {
		$attributes = $scanned_object->attributes();
		$content    = isset( $attributes['content'] ) && is_string( $attributes['content'] ) ? $attributes['content'] : '';
		if ( 'file' !== $scanned_object->type() || ! PhpSource::has_open_tag( $content ) ) {
			return array();
		}
		$signals  = $this->analyzer->analyze( $content );
		$evidence = array();
		foreach ( $this->rules as $rule ) {
			$data    = $rule->data();
			$sources = isset( $data['sources'] ) && is_array( $data['sources'] ) ? $data['sources'] : array();
			$sinks   = isset( $data['sinks'] ) && is_array( $data['sinks'] ) ? $data['sinks'] : array();
			$flow    = isset( $data['flow'] ) && is_string( $data['flow'] ) ? $data['flow'] : '';
			if ( $signals['decoder_execution'] && $this->intersects( $sources, array( 'base64_decode', 'gzinflate', 'gzuncompress', 'str_rot13' ) ) && $this->intersects( $sinks, array( 'eval', 'include', 'include_once', 'require', 'require_once' ) ) ) {
				$evidence[] = new Evidence( $rule->id(), $rule->weight(), $rule->confidence(), $rule->message_key(), 'proven-decoder-to-execution' );
			} elseif ( ( ( '' === $flow && $signals['request_write'] ) || ( 'request_controlled_path' === $flow && $signals['request_controlled_path'] ) || ( 'request_executable_write' === $flow && $signals['request_executable_write'] ) ) && $this->intersects( $sources, array( '_GET', '_POST', '_REQUEST', '_COOKIE', '_FILES' ) ) && $this->intersects( $sinks, array( 'file_put_contents', 'fopen', 'fwrite', 'move_uploaded_file' ) ) ) {
				$evidence[] = new Evidence( $rule->id(), $rule->weight(), $rule->confidence(), $rule->message_key(), 'proven-request-to-executable-write' );
			}
		}
		return $evidence;
	}

	/**
	 * @param mixed[]  $left  Left values.
	 * @param string[] $right Right values.
	 * @return bool
	 */
	private function intersects( array $left, array $right ) {
		return ! empty( array_intersect( $left, $right ) );
	}
}
