<?php

use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;

/**
 * AST visitor that detects suspicious PHP constructs indicative of malware.
 * Loaded at runtime only when nikic/php-parser is available (--ast flag).
 */
class MalwareScannerAstVisitor extends NodeVisitorAbstract
{
    private $findings = array();

    public function enterNode(Node $node)
    {
        // eval() with any non-trivial argument
        if ($node instanceof Node\Expr\Eval_) {
            if (!($node->expr instanceof Node\Scalar\String_)
                || strlen($node->expr->value) > 0) {
                $this->findings[] = array(
                    'type'   => 'dynamic-eval',
                    'detail' => 'eval() call — code execution from string',
                    'line'   => $node->getLine(),
                );
            }
        }

        if (!($node instanceof Node\Expr\FuncCall)) {
            return;
        }

        // Dynamic function call via variable: $func(...)
        if ($node->name instanceof Node\Expr\Variable) {
            $this->findings[] = array(
                'type'   => 'dynamic-call',
                'detail' => 'Dynamic function call via variable $' . $node->name->name . '(...)',
                'line'   => $node->getLine(),
            );
            return;
        }

        if (!($node->name instanceof Node\Name)) {
            return;
        }

        $fname = strtolower($node->name->toString());

        // create_function('', $code) — runtime code compilation
        if ($fname === 'create_function') {
            $this->findings[] = array(
                'type'   => 'create_function',
                'detail' => 'create_function() — deprecated runtime code compilation',
                'line'   => $node->getLine(),
            );
        }

        // assert() with dynamic argument — code execution risk (PHP < 8)
        if ($fname === 'assert' && count($node->args) > 0) {
            $arg = $node->args[0];
            if ($arg instanceof Node\Arg) {
                $val = $arg->value;
                $isStaticBool = ($val instanceof Node\Expr\ConstFetch);
                if (!$isStaticBool) {
                    $this->findings[] = array(
                        'type'   => 'assert-exec',
                        'detail' => 'assert() with dynamic argument — evaluates as code in PHP < 8',
                        'line'   => $node->getLine(),
                    );
                }
            }
        }

        // preg_replace() with /e modifier — executes second argument as PHP code
        if ($fname === 'preg_replace' && count($node->args) > 0) {
            $arg = $node->args[0];
            if ($arg instanceof Node\Arg
                && $arg->value instanceof Node\Scalar\String_) {
                $pattern = $arg->value->value;
                if (preg_match('/\be\b/i', ltrim(strrchr($pattern, '/'), '/'))) {
                    $this->findings[] = array(
                        'type'   => 'preg_replace-e',
                        'detail' => 'preg_replace() with /e modifier — arbitrary code execution via regex',
                        'line'   => $node->getLine(),
                    );
                }
            }
        }
    }

    public function getFindings()
    {
        return $this->findings;
    }
}
