Skip to content

Commit

Permalink
Merge pull request #37 from angelej/level
Browse files Browse the repository at this point in the history
Introduce levels
  • Loading branch information
angelej authored Sep 1, 2023
2 parents adbde7d + ceb6b62 commit 9598565
Show file tree
Hide file tree
Showing 42 changed files with 444 additions and 94 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ insider@linux:~$ docker run --rm -it -v /path/to/app:/app angelej/php-insider:la
Summary: 1 sink found
```

## Level
The level can be defined using the `-l|--level` command option.
The higher the level, the more selective the analysis.

| Level | Description |
|:-------------------|:-------------------------------------------|
| **0** (_default_) | all supported sinks |
| **1** | sinks with dynamic variables |

## Supported Sinks
### Code Execution
- [`` `backtick` ``](https://www.php.net/manual/en/language.operators.execution)
Expand All @@ -73,7 +82,7 @@ insider@linux:~$ docker run --rm -it -v /path/to/app:/app angelej/php-insider:la
### File Write
- [`copy()`](https://www.php.net/manual/en/function.copy)
- [`file_put_contents()`](https://www.php.net/manual/en/function.file-put-contents)
- [`move_uploaded_file()`](https://www.php.net/manual/de/function.move-uploaded-file)
- [`move_uploaded_file()`](https://www.php.net/manual/en/function.move-uploaded-file)

### Information Disclosure
- [`phpinfo()`](https://www.php.net/manual/en/function.phpinfo)
Expand Down
17 changes: 15 additions & 2 deletions src/Analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class Analyser {
*/
protected Parser $parser;

/** @var int */
protected int $level = 0;

public function __construct(){

$this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
Expand All @@ -28,7 +31,7 @@ public function analyse(array|File $files): Report {
if(!is_array($files)) $files = [$files];

$traverser = new NodeTraverser();
$sinkDetector = new SinkDetector();
$sinkDetector = (new SinkDetector())->setLevel($this->level);
$traverser->addVisitor($sinkDetector);

foreach($files as $file){
Expand All @@ -43,4 +46,14 @@ public function analyse(array|File $files): Report {
}
return Report::getInstance();
}
}

/**
* @param int $level
* @return $this
*/
public function setLevel(int $level): self {

$this->level = $level;
return $this;
}
}
29 changes: 27 additions & 2 deletions src/Commands/AnalyseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Angelej\PhpInsider\Commands;

use Angelej\PhpInsider\File;
use Angelej\PhpInsider\Level;
use Angelej\PhpInsider\Analyser;
use Angelej\PhpInsider\Sinks\Sink;
use Angelej\PhpInsider\LocationHelper;
Expand All @@ -12,6 +13,7 @@
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Exception\InvalidOptionException;
use function Termwind\render;

#[AsCommand(
Expand All @@ -33,8 +35,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$excludedFiles = $input->getOption('exclude-file');
$files = is_dir($file) ? File::glob($file, $extensions, $excludedFiles) : new File($file);
$expandLines = max((int) $input->getOption('lines'), 0);
$level = $this->getLevelOption($input);

$report = (new Analyser())->analyse($files);
$report = (new Analyser())
->setLevel($level)
->analyse($files);
$sinks = $report->get();

foreach($sinks as $sink){
Expand All @@ -48,6 +53,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int
: Command::SUCCESS;
}

/**
* @param \Symfony\Component\Console\Input\InputInterface $input
* @return int
*/
private function getLevelOption(InputInterface $input): int {

$level = (int) $input->getOption('level');
$min = Level::min()->value;
$max = Level::max()->value;

if($level < $min || $level > $max){
throw new InvalidOptionException('Invalid "--level" value provided. Level must be between ' . $min . ' and ' . $max . '.');
}
return $level;
}

/**
* @param \Symfony\Component\Console\Input\InputInterface $input
* @return string[]
Expand Down Expand Up @@ -99,10 +120,14 @@ private function printSummary(array $sinks): void {
/** @return void */
protected function configure(): void {

$minLevel = Level::min()->value;
$maxLevel = Level::max()->value;

$this
->addArgument('file', InputArgument::REQUIRED, 'File or directory path to analyse')
->addOption('level', '-l', InputOption::VALUE_REQUIRED, 'Level of analysis [' . $minLevel . '-' . $maxLevel . ']. The higher the level, the more selective the analysis', 0)
->addOption('extension', '-e', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'File extension', ['php'])
->addOption('exclude-file', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'File or directory to exclude', [])
->addOption('lines', '-l', InputOption::VALUE_REQUIRED, 'Number of lines to expand code snippet', 2);
->addOption('lines', null, InputOption::VALUE_REQUIRED, 'Number of lines to expand code snippet', 2);
}
}
21 changes: 21 additions & 0 deletions src/Level.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);

namespace Angelej\PhpInsider;

enum Level: int {

CASE ZERO = 0;
CASE ONE = 1;

/** @return \Angelej\PhpInsider\Level */
public static function min(): self {

return self::ZERO;
}

/** @return \Angelej\PhpInsider\Level */
public static function max(): self {

return self::ONE;
}
}
18 changes: 18 additions & 0 deletions src/Report.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ final class Report {
*/
private ?int $inLine = null;

/** @var \Angelej\PhpInsider\Level|null */
private ?Level $ofLevel = null;

/**
* @var \Angelej\PhpInsider\Sinks\Sink[]
*/
Expand Down Expand Up @@ -67,6 +70,16 @@ public function ofSink(string $sink): self {
return $this;
}

/**
* @param \Angelej\PhpInsider\Level $level
* @return self
*/
public function ofLevel(Level $level): self {

$this->ofLevel = $level;
return $this;
}

/**
* @param int $line
* @return $this
Expand Down Expand Up @@ -97,13 +110,18 @@ public function get(): array {
if($this->inLine && $sink->getLocation()->getLine() !== $this->inLine){
continue;
}

if($this->ofLevel && $sink->getLevel() !== $this->ofLevel){
continue;
}
$result[] = $sink;
}

// reset criteria
$this->inFile = null;
$this->inLine = null;
$this->ofSink = null;
$this->ofLevel = null;

return $result;
}
Expand Down
19 changes: 17 additions & 2 deletions src/SinkDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ class SinkDetector extends NodeVisitorAbstract {
*/
protected Report $report;

/** @var int */
protected int $level = 0;

public function __construct(){

$this->report = Report::getInstance();
Expand All @@ -56,9 +59,11 @@ public function enterNode(Node $node): Node {

foreach($this->sinks as $sinkName){

if($sinkName::is($node)){
$level = $sinkName::is($node);

if($level instanceof Level && $level->value >= $this->level){

$sink = new $sinkName(clone $this->currentLocation);
$sink = new $sinkName(clone $this->currentLocation, $level);
$this->report->add($sink);
}
}
Expand Down Expand Up @@ -99,4 +104,14 @@ public function setLocation(Location $location): self {
$this->currentLocation = $location;
return $this;
}

/**
* @param int $level
* @return $this
*/
public function setLevel(int $level): self {

$this->level = $level;
return $this;
}
}
17 changes: 14 additions & 3 deletions src/Sinks/CodeExecution/BacktickSink.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Angelej\PhpInsider\Sinks\CodeExecution;

use PhpParser\Node;
use Angelej\PhpInsider\Level;
use PhpParser\Node\Expr\ShellExec;
use Angelej\PhpInsider\Sinks\Sink;
use Angelej\PhpInsider\NodeHelper;
Expand All @@ -11,10 +12,20 @@ class BacktickSink extends Sink {

/**
* @param \PhpParser\Node $node
* @return bool
* @return \Angelej\PhpInsider\Level|null
*/
public static function is(Node $node): bool {
public static function is(Node $node): ?Level {

return $node instanceof ShellExec && NodeHelper::isDynamic($node);
$level = null;

if($node instanceof ShellExec){

$level = Level::ZERO;

if(NodeHelper::isDynamic($node)){
$level = Level::ONE;
}
}
return $level;
}
}
17 changes: 14 additions & 3 deletions src/Sinks/CodeExecution/EvalSink.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Angelej\PhpInsider\Sinks\CodeExecution;

use PhpParser\Node;
use Angelej\PhpInsider\Level;
use PhpParser\Node\Expr\Eval_;
use Angelej\PhpInsider\Sinks\Sink;
use Angelej\PhpInsider\NodeHelper;
Expand All @@ -11,10 +12,20 @@ class EvalSink extends Sink {

/**
* @param \PhpParser\Node $node
* @return bool
* @return \Angelej\PhpInsider\Level|null
*/
public static function is(Node $node): bool {
public static function is(Node $node): ?Level {

return $node instanceof Eval_ && NodeHelper::isDynamic($node);
$level = null;

if($node instanceof Eval_){

$level = Level::ZERO;

if(NodeHelper::isDynamic($node)){
$level = Level::ONE;
}
}
return $level;
}
}
18 changes: 14 additions & 4 deletions src/Sinks/CodeExecution/ExecSink.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,28 @@
namespace Angelej\PhpInsider\Sinks\CodeExecution;

use PhpParser\Node;
use Angelej\PhpInsider\Level;
use Angelej\PhpInsider\Sinks\Sink;
use Angelej\PhpInsider\NodeHelper;

class ExecSink extends Sink {

/**
* @param \PhpParser\Node $node
* @return bool
* @return \Angelej\PhpInsider\Level|null
*/
public static function is(Node $node): bool {
public static function is(Node $node): ?Level {

return NodeHelper::isFunctionCall($node, 'exec')
&& NodeHelper::isDynamic($node->args[0] ?? null);
$level = null;

if(NodeHelper::isFunctionCall($node, 'exec')){

$level = Level::ZERO;

if(NodeHelper::isDynamic($node->args[0] ?? null)){
$level = Level::ONE;
}
}
return $level;
}
}
18 changes: 14 additions & 4 deletions src/Sinks/CodeExecution/PassthruSink.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,28 @@
namespace Angelej\PhpInsider\Sinks\CodeExecution;

use PhpParser\Node;
use Angelej\PhpInsider\Level;
use Angelej\PhpInsider\Sinks\Sink;
use Angelej\PhpInsider\NodeHelper;

class PassthruSink extends Sink {

/**
* @param \PhpParser\Node $node
* @return bool
* @return \Angelej\PhpInsider\Level|null
*/
public static function is(Node $node): bool {
public static function is(Node $node): ?Level {

return NodeHelper::isFunctionCall($node, 'passthru')
&& NodeHelper::isDynamic($node->args[0] ?? null);
$level = null;

if(NodeHelper::isFunctionCall($node, 'passthru')){

$level = Level::ZERO;

if(NodeHelper::isDynamic($node->args[0] ?? null)){
$level = Level::ONE;
}
}
return $level;
}
}
Loading

0 comments on commit 9598565

Please sign in to comment.