├── bin ├── parse └── psecio-parse ├── .gitignore ├── .travis.yml ├── src ├── Rule │ ├── Helper │ │ ├── NameTrait.php │ │ ├── IsExpressionTrait.php │ │ ├── IsBoolLiteralTrait.php │ │ ├── DocblockDescriptionTrait.php │ │ └── IsFunctionCallTrait.php │ ├── EvalFunction.php │ ├── RequestUse.php │ ├── GlobalsUse.php │ ├── RunkitImport.php │ ├── HttpRawPostData.php │ ├── Readfile.php │ ├── MysqlRealEscapeString.php │ ├── ImportRequestVariables.php │ ├── EregFunctions.php │ ├── ParseStr.php │ ├── SystemFunctions.php │ ├── SessionRegenerateId.php │ ├── SetHeaderWithInput.php │ ├── BooleanIdentity.php │ ├── ExitOrDie.php │ ├── LogicalOperators.php │ ├── TypeSafeInArray.php │ ├── EchoWithFileGetContents.php │ ├── InArrayStrict.php │ ├── Extract.php │ ├── OutputWithVariable.php │ ├── DisplayErrors.php │ ├── PregReplaceWithEvalModifier.php │ └── HardcodedSensitiveValues.php ├── DocComment │ ├── DocCommentFactoryInterface.php │ ├── DocCommentFactory.php │ ├── DocCommentInterface.php │ └── DocComment.php ├── Event │ ├── MessageEvent.php │ ├── FileEvent.php │ ├── ErrorEvent.php │ ├── IssueEvent.php │ └── Events.php ├── Subscriber │ ├── OutputTrait.php │ ├── ExitCodeCatcher.php │ ├── ConsoleDebug.php │ ├── ConsoleLines.php │ ├── Json.php │ ├── ConsoleProgressBar.php │ ├── ConsoleDots.php │ ├── Xml.php │ ├── Subscriber.php │ └── ConsoleReport.php ├── RuleInterface.php ├── RuleFactory.php ├── File.php ├── RuleCollection.php ├── Scanner.php ├── Command │ ├── RulesCommand.php │ └── ScanCommand.php ├── CallbackVisitor.php └── FileIterator.php ├── tests ├── Event │ ├── MessageEventTest.php │ ├── FileEventTest.php │ ├── ErrorEventTest.php │ └── IssueEventTest.php ├── Rule │ ├── HttpRawPostDataTest.php │ ├── GlobalsUseTest.php │ ├── ImportRequestVariablesTest.php │ ├── RunkitImportTest.php │ ├── OutputWithVariableTest.php │ ├── RequestUseTest.php │ ├── MysqlRealEscapeStringTest.php │ ├── EvalFunctionTest.php │ ├── ReadfileTest.php │ ├── InArrayStrictTest.php │ ├── TypeSafeInArrayTest.php │ ├── LogicalOperatorsTest.php │ ├── EchoWithFileGetContentsTest.php │ ├── SystemFunctionsTest.php │ ├── BooleanIdentityTest.php │ ├── ParseStrTest.php │ ├── EregFunctionsTest.php │ ├── ExtractTest.php │ ├── SessionRegenerateIdTest.php │ ├── ExitOrDieTest.php │ ├── PregReplaceWithEvalModifierTest.php │ ├── RuleTestVisitor.php │ ├── DisplayErrorsTest.php │ ├── HardcodedSensitiveValuesTest.php │ └── RuleTestCase.php ├── IntegrationTest.php ├── DocComment │ ├── DocCommentFactoryTest.php │ └── DocCommentTest.php ├── Subscriber │ ├── ExitCodeCatcherTest.php │ ├── ConsoleProgressBarTest.php │ ├── SubscriberTest.php │ ├── ConsoleDebugTest.php │ ├── ConsoleDotsTest.php │ ├── XmlTest.php │ ├── ConsoleLinesTest.php │ └── ConsoleReportTest.php ├── Fakes │ ├── FakeDocCommentFactory.php │ ├── FakeRule.php │ ├── FakeDocComment.php │ └── FakeNode.php ├── Command │ ├── RulesCommandTest.php │ └── ScanCommandTest.php ├── RuleFactoryTest.php ├── FileTest.php ├── RuleCollectionTest.php ├── ScannerTest.php ├── FileIteratorTest.php └── CallbackVisitorTest.php ├── examples └── annotations │ ├── turnOffRule.php │ └── turnOnRule.php ├── phpunit.xml ├── composer.json ├── CONTRIBUTING.md └── README.md /bin/parse: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | `dirname $0`/psecio-parse "$@" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | /bin/coveralls 4 | /bin/phpunit 5 | /bin/test-reporter 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: trusty 4 | 5 | php: 6 | - hhvm 7 | - 7.0 8 | - 5.6 9 | - 5.5 10 | - 5.4 11 | 12 | install: 13 | - composer install --prefer-source 14 | 15 | script: 16 | - bin/phpunit 17 | - bin/psecio-parse scan src tests 18 | -------------------------------------------------------------------------------- /src/Rule/Helper/NameTrait.php: -------------------------------------------------------------------------------- 1 | assertSame( 12 | 'my message', 13 | (new MessageEvent('my message'))->getMessage() 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/annotations/turnOffRule.php: -------------------------------------------------------------------------------- 1 | assertSame( 13 | $file, 14 | (new FileEvent($file))->getFile() 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Event/ErrorEventTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 12 | 'my message', 13 | (new ErrorEvent('my message', m::mock('\Psecio\Parse\File')))->getMessage() 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Rule/HttpRawPostDataTest.php: -------------------------------------------------------------------------------- 1 | add(new ScanCommand); 16 | $app->add(new RulesCommand); 17 | $app->run(); 18 | -------------------------------------------------------------------------------- /tests/Rule/LogicalOperatorsTest.php: -------------------------------------------------------------------------------- 1 | isExpression($node, 'Eval'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Rule/EchoWithFileGetContentsTest.php: -------------------------------------------------------------------------------- 1 | name == '_REQUEST'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Rule/GlobalsUse.php: -------------------------------------------------------------------------------- 1 | name == 'GLOBALS'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Rule/RunkitImport.php: -------------------------------------------------------------------------------- 1 | isFunctionCall($node, 'runkit_import'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Rule/SystemFunctionsTest.php: -------------------------------------------------------------------------------- 1 | name == 'http_raw_post_data'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Rule/Readfile.php: -------------------------------------------------------------------------------- 1 | isFunctionCall($node, ['readfile', 'readlink', 'readgzfile']); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Rule/BooleanIdentityTest.php: -------------------------------------------------------------------------------- 1 | isFunctionCall($node, 'mysql_real_escape_string'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Event/IssueEventTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 18 | $rule, 19 | $event->getRule() 20 | ); 21 | 22 | $this->assertSame( 23 | $node, 24 | $event->getNode() 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Rule/ImportRequestVariables.php: -------------------------------------------------------------------------------- 1 | isFunctionCall($node, 'import_request_variables'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Event/MessageEvent.php: -------------------------------------------------------------------------------- 1 | message = $message; 25 | } 26 | 27 | /** 28 | * Get message 29 | * 30 | * @return string 31 | */ 32 | public function getMessage() 33 | { 34 | return $this->message; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Rule/EregFunctions.php: -------------------------------------------------------------------------------- 1 | isFunctionCall($node, ['ereg', 'eregi', 'ereg_replace', 'eregi_replace'])) { 22 | return false; 23 | } 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Rule/ParseStrTest.php: -------------------------------------------------------------------------------- 1 | isFunctionCall($node, ['parse_str', 'mb_parse_str'])) { 22 | return true; 23 | } 24 | 25 | return $this->countCalledFunctionArguments($node) > 1; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Rule/EregFunctionsTest.php: -------------------------------------------------------------------------------- 1 | file = $file; 26 | } 27 | 28 | /** 29 | * Get File object this event conserns 30 | * 31 | * @return File 32 | */ 33 | public function getFile() 34 | { 35 | return $this->file; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Rule/SystemFunctions.php: -------------------------------------------------------------------------------- 1 | isFunctionCall($node, ['exec', 'passthru', 'system'])) { 22 | return false; 23 | } 24 | 25 | return !$this->isExpression($node, 'ShellExec'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/IntegrationTest.php: -------------------------------------------------------------------------------- 1 | message = $message; 27 | } 28 | 29 | /** 30 | * Get message 31 | * 32 | * @return string 33 | */ 34 | public function getMessage() 35 | { 36 | return $this->message; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Rule/Helper/IsExpressionTrait.php: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Rule/SessionRegenerateId.php: -------------------------------------------------------------------------------- 1 | isFunctionCall($node, 'session_regenerate_id')) { 22 | return $this->isBoolLiteral($this->getCalledFunctionArgument($node, 0)->value, true); 23 | } 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/DocComment/DocCommentFactoryTest.php: -------------------------------------------------------------------------------- 1 | createDocComment(self::COMMENT); 17 | $this->assertInstanceOf(__NAMESPACE__ . '\\DocCommentInterface', $docComment); 18 | } 19 | 20 | public function testCreateDocCommentSavesRawComment() 21 | { 22 | $factory = new DocCommentFactory; 23 | $docComment = $factory->createDocComment(self::COMMENT); 24 | $this->assertSame(self::COMMENT, $docComment->getRawComment()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Rule/SetHeaderWithInput.php: -------------------------------------------------------------------------------- 1 | isFunctionCall($node, 'header')) { 23 | if ($this->getCalledFunctionArgument($node, 0)->value instanceof Concat) { 24 | return false; 25 | } 26 | } 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Rule/BooleanIdentity.php: -------------------------------------------------------------------------------- 1 | isBoolLiteral($node->left) || $this->isBoolLiteral($node->right)) { 24 | return false; 25 | } 26 | } 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Subscriber/OutputTrait.php: -------------------------------------------------------------------------------- 1 | output = $output; 25 | } 26 | 27 | /** 28 | * Write to console 29 | * 30 | * @param mixed ...$arg Any number of sprintf arguments 31 | * @return void 32 | */ 33 | protected function write() 34 | { 35 | $this->output->write(call_user_func_array('sprintf', func_get_args())); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Subscriber/ExitCodeCatcherTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 12 | 0, 13 | (new ExitCodeCatcher)->getExitCode() 14 | ); 15 | } 16 | 17 | public function testErrorcodeOnIssue() 18 | { 19 | $exitCode = new ExitCodeCatcher; 20 | $exitCode->onFileIssue(m::mock('\Psecio\Parse\Event\IssueEvent')); 21 | $this->assertSame(1, $exitCode->getExitCode()); 22 | } 23 | 24 | public function testErrorcodeOnError() 25 | { 26 | $exitCode = new ExitCodeCatcher; 27 | $exitCode->onFileError(m::mock('\Psecio\Parse\Event\ErrorEvent')); 28 | $this->assertSame(1, $exitCode->getExitCode()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Fakes/FakeDocCommentFactory.php: -------------------------------------------------------------------------------- 1 | commentList[$comment]; 21 | } 22 | 23 | /** 24 | * Add a doc comment from a given comment to be parsed 25 | * 26 | * @param string $comment 27 | * @param string $docComment 28 | */ 29 | public function addDocComment($comment, $docComment) 30 | { 31 | $this->commentList[$comment] = $docComment; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Subscriber/ConsoleProgressBarTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getMaxSteps')->ordered()->once(); 15 | $bar->shouldReceive('setFormat')->ordered()->once(); 16 | $bar->shouldReceive('start')->ordered()->once(); 17 | $bar->shouldReceive('advance')->ordered()->once(); 18 | $bar->shouldReceive('finish')->ordered()->once(); 19 | 20 | $console = new ConsoleProgressBar($bar); 21 | $console->onScanStart(); 22 | $console->onFileClose(); 23 | $console->onScanComplete(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Rule/ExitOrDie.php: -------------------------------------------------------------------------------- 1 | isExpression($node, 'Exit') === true) { 25 | if ($node->expr instanceof Concat) { 26 | return false; 27 | } 28 | } 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Rule/ExitOrDieTest.php: -------------------------------------------------------------------------------- 1 | name = $name; 16 | $this->validNodes = $validNodes; 17 | } 18 | 19 | public function getName() 20 | { 21 | return $this->name; 22 | } 23 | 24 | public function getDescription() 25 | { 26 | return ''; 27 | } 28 | 29 | public function getLongDescription() 30 | { 31 | return ''; 32 | } 33 | 34 | public function isValid(Node $node) 35 | { 36 | $valid = in_array($node, $this->validNodes, true); 37 | } 38 | 39 | public function addValidNode($node) 40 | { 41 | $this->validNodes[] = $node; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/annotations/turnOnRule.php: -------------------------------------------------------------------------------- 1 | assertInternalType( 12 | 'array', 13 | Subscriber::getSubscribedEvents() 14 | ); 15 | } 16 | 17 | public function testEmptyMethods() 18 | { 19 | $subscriber = new Subscriber; 20 | $this->assertNull($subscriber->onScanStart()); 21 | $this->assertNull($subscriber->onScanComplete()); 22 | $this->assertNull($subscriber->onFileOpen(m::mock('\Psecio\Parse\Event\FileEvent'))); 23 | $this->assertNull($subscriber->onFileClose()); 24 | $this->assertNull($subscriber->onFileIssue(m::mock('\Psecio\Parse\Event\IssueEvent'))); 25 | $this->assertNull($subscriber->onFileError(m::mock('\Psecio\Parse\Event\ErrorEvent'))); 26 | $this->assertNull($subscriber->onDebug(m::mock('\Psecio\Parse\Event\MessageEvent'))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Rule/TypeSafeInArray.php: -------------------------------------------------------------------------------- 1 | getCalledFunctionName($node) === 'in_array') { 24 | if (count($node->args) == 2) { 25 | return false; 26 | } elseif (count($node->args) == 3 && $this->isBoolLiteral($node->args[2]->value, false)) { 27 | return false; 28 | } 29 | } 30 | 31 | return true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Rule/PregReplaceWithEvalModifierTest.php: -------------------------------------------------------------------------------- 1 | exitCode; 26 | } 27 | 28 | /** 29 | * Set exit code 1 on file issue 30 | * 31 | * @param IssueEvent $event 32 | * @return void 33 | */ 34 | public function onFileIssue(IssueEvent $event) 35 | { 36 | $this->exitCode = 1; 37 | } 38 | 39 | /** 40 | * Set exit code 1 on file error 41 | * 42 | * @param ErrorEvent $event 43 | * @return void 44 | */ 45 | public function onFileError(ErrorEvent $event) 46 | { 47 | $this->exitCode = 1; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/RuleInterface.php: -------------------------------------------------------------------------------- 1 | 34 | * 35 | * @return string 36 | */ 37 | public function getLongDescription(); 38 | 39 | /** 40 | * Check if node is valid 41 | * 42 | * @param Node $node 43 | * @return boolean 44 | */ 45 | public function isValid(Node $node); 46 | } 47 | -------------------------------------------------------------------------------- /tests/Rule/RuleTestVisitor.php: -------------------------------------------------------------------------------- 1 | rule = $rule; 36 | } 37 | 38 | /** 39 | * Evaluate a node 40 | * 41 | * @param Node $node The node to evaluate 42 | */ 43 | public function enterNode(Node $node) 44 | { 45 | if (!$this->rule->isValid($node)) { 46 | $this->result = false; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Rule/Helper/IsBoolLiteralTrait.php: -------------------------------------------------------------------------------- 1 | name) && $node->name instanceof \PhpParser\Node\Name) { 24 | $name = strtolower($node->name); 25 | if ($name === 'true' || $name === 'false') { 26 | if ($value === true) { 27 | return $name === 'true'; 28 | } elseif ($value === false) { 29 | return $name === 'false'; 30 | } 31 | return true; 32 | } 33 | } 34 | return false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Rule/EchoWithFileGetContents.php: -------------------------------------------------------------------------------- 1 | exprs[0]) && $node->exprs[0] instanceof Concat) { 25 | // Check the right side 26 | if ($this->isFunctionCall($node->exprs[0]->right, 'file_get_contents')) { 27 | return false; 28 | } 29 | // Check the left side 30 | if ($this->isFunctionCall($node->exprs[0]->left, 'file_get_contents')) { 31 | return false; 32 | } 33 | } 34 | } 35 | 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Rule/DisplayErrorsTest.php: -------------------------------------------------------------------------------- 1 | isFunctionCall($node, 'in_array') === true) { 22 | 23 | // Be sure there's three params 24 | if (count($node->args) < 3) { 25 | return false; 26 | } 27 | 28 | // Be sure it's a boolean value 29 | if (!$this->isBoolLiteral($node->args[2]->value)) { 30 | return false; 31 | } 32 | 33 | // Be sure the value is "true" 34 | if ((string)$node->args[2]->value->name !== 'true') { 35 | return false; 36 | } 37 | 38 | return true; 39 | } 40 | 41 | return true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"psecio/parse", 3 | "type":"library", 4 | "description":"A PHP Security Scanner", 5 | "keywords":["security","scanner","static"], 6 | "homepage":"https://github.com/psecio/parse.git", 7 | "license":"MIT", 8 | "authors":[ 9 | { 10 | "name":"Chris Cornutt", 11 | "email":"ccornutt@phpdeveloper.org", 12 | "homepage":"http://www.phpdeveloper.org/" 13 | } 14 | ], 15 | "require":{ 16 | "php": ">=5.4", 17 | "nikic/php-parser": "^2.0", 18 | "symfony/console": "2.5 - 3.2", 19 | "symfony/event-dispatcher": "2.4 - 3.4" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^4.2", 23 | "codeclimate/php-test-reporter": "dev-master", 24 | "mockery/mockery": "^0.9", 25 | "akamon/mockery-callable-mock": "^1.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Psecio\\Parse\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Psecio\\Parse\\": "tests/" 35 | } 36 | }, 37 | "config": { 38 | "bin-dir": "bin" 39 | }, 40 | "bin": [ 41 | "bin/psecio-parse", 42 | "bin/parse" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/Event/IssueEvent.php: -------------------------------------------------------------------------------- 1 | rule = $rule; 35 | $this->node = $node; 36 | } 37 | 38 | /** 39 | * Get rule object ths event conserns 40 | * 41 | * @return RuleInterface 42 | */ 43 | public function getRule() 44 | { 45 | return $this->rule; 46 | } 47 | 48 | /** 49 | * Get Node object ths event conserns 50 | * 51 | * @return Node 52 | */ 53 | public function getNode() 54 | { 55 | return $this->node; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Subscriber/ConsoleDebugTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('write')->ordered()->once()->with("[DEBUG] Starting scan\n"); 15 | $output->shouldReceive('write')->ordered()->once()->with("[DEBUG] debug message\n"); 16 | $output->shouldReceive('write')->ordered()->once()->with("/\[DEBUG\] Scan completed in \d+\.\d+ seconds/"); 17 | 18 | // Data for debug event 19 | $messageEvent = m::mock('\Psecio\Parse\Event\MessageEvent'); 20 | $messageEvent->shouldReceive('getMessage')->andReturn('debug message'); 21 | 22 | $console = new ConsoleDebug($output); 23 | 24 | // Should write debug start 25 | $console->onScanStart(); 26 | 27 | // Writes debug message 28 | $console->onDebug($messageEvent); 29 | 30 | // Writes time used an extra new line 31 | $console->onScanComplete(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Rule/Extract.php: -------------------------------------------------------------------------------- 1 | isFunctionCall($node, 'extract')) { 25 | // Check to be sure it has two arguments 26 | if ($this->countCalledFunctionArguments($node) < 2) { 27 | return false; 28 | } 29 | 30 | $arg = $this->getCalledFunctionArgument($node, 1); 31 | 32 | $name = (isset($arg->value->name->parts[0])) 33 | ? $arg->value->name->parts[0] 34 | : $arg->value->name; 35 | 36 | // So we have two parameters...see if #2 is not equal to EXTR_OVERWRITE 37 | return $name !== 'EXTR_OVERWRITE'; 38 | } 39 | return true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Rule/Helper/DocblockDescriptionTrait.php: -------------------------------------------------------------------------------- 1 | docblock)) { 26 | $this->docblock = new DocComment( 27 | (new ReflectionClass($this))->getDocComment() 28 | ); 29 | } 30 | 31 | return $this->docblock; 32 | } 33 | 34 | /** 35 | * Returns the summary of the class level doc comment 36 | * 37 | * @return string 38 | */ 39 | public function getDescription() 40 | { 41 | return $this->getDocblock()->getSummary(); 42 | } 43 | 44 | /** 45 | * Returns the body of the class level doc comment 46 | * 47 | * @return string 48 | */ 49 | public function getLongDescription() 50 | { 51 | return $this->getDocblock()->getBody(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/DocComment/DocCommentInterface.php: -------------------------------------------------------------------------------- 1 | write("[DEBUG] Starting scan\n"); 26 | $this->startTime = microtime(true); 27 | } 28 | 29 | /** 30 | * Write time elapsed at scan complete 31 | * 32 | * @return void 33 | */ 34 | public function onScanComplete() 35 | { 36 | $this->write( 37 | "[DEBUG] Scan completed in %f seconds\n", 38 | microtime(true) - $this->startTime 39 | ); 40 | parent::onScanComplete(); 41 | } 42 | 43 | /** 44 | * Write debug message 45 | * 46 | * @param MessageEvent $event 47 | * @return void 48 | */ 49 | public function onDebug(MessageEvent $event) 50 | { 51 | $this->write( 52 | "[DEBUG] %s\n", 53 | $event->getMessage() 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Command/RulesCommandTest.php: -------------------------------------------------------------------------------- 1 | add(new RulesCommand); 17 | $command = $application->find('rules'); 18 | $commandTester = new CommandTester($command); 19 | 20 | $commandTester->execute(['command' => $command->getName()]); 21 | 22 | $this->assertRegExp( 23 | '/Description/i', 24 | $commandTester->getDisplay(), 25 | 'The rules command should produce output' 26 | ); 27 | } 28 | 29 | public function testDescribeRule() 30 | { 31 | $application = new Application; 32 | $application->add(new RulesCommand); 33 | $command = $application->find('rules'); 34 | $commandTester = new CommandTester($command); 35 | 36 | $commandTester->execute(['command' => $command->getName(), 'rule' => 'exitordie']); 37 | 38 | $this->assertRegExp( 39 | '/ExitOrDie/', 40 | $commandTester->getDisplay(), 41 | 'The rules command should produce output' 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Subscriber/ConsoleLines.php: -------------------------------------------------------------------------------- 1 | write("[PARSE] %s\n", $event->getFile()->getPath()); 25 | } 26 | 27 | /** 28 | * Write issue as one line 29 | * 30 | * @param IssueEvent $event 31 | * @return void 32 | */ 33 | public function onFileIssue(IssueEvent $event) 34 | { 35 | $this->write( 36 | "[ISSUE] [%s] On line %d in %s\n", 37 | $event->getRule()->getName(), 38 | $event->getNode()->getLine(), 39 | $event->getFile()->getPath() 40 | ); 41 | } 42 | 43 | /** 44 | * Write error as one line 45 | * 46 | * @param ErrorEvent $event 47 | * @return void 48 | */ 49 | public function onFileError(ErrorEvent $event) 50 | { 51 | $this->write( 52 | "[ERROR] %s in %s\n", 53 | $event->getMessage(), 54 | $event->getFile()->getPath() 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Rule/OutputWithVariable.php: -------------------------------------------------------------------------------- 1 | exprs[0]) && $node->exprs[0] instanceof Concat) { 28 | // See if either of the items is a variable 29 | if ($node->exprs[0]->left instanceof Variable || $node->exprs[0]->right instanceof Variable) { 30 | return false; 31 | } 32 | } 33 | } 34 | 35 | // See if our other output functions use concat 36 | if ($this->isFunctionCall($node, ['print_r', 'printf', 'vprintf', 'sprintf'])) { 37 | if ($this->getCalledFunctionArgument($node, 0)->value instanceof Concat) { 38 | return false; 39 | } 40 | } 41 | 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Event/Events.php: -------------------------------------------------------------------------------- 1 | 22 | 23 | That is, we name what the rulw is about and potentially add some modifiers saying when it should be applied.. 24 | 25 | An example of just a what would be `EvalFunction`. Since `eval()` is (almost) always bad, we don't need to modify the name. Another would be `BooleanIdentity`, which enforces always using the identical operator (`===`) instead of the equals operator (`==`). There's not much more involved in the concept so a modifier isn't needed. 26 | 27 | An example of a when modifier is `EchoWithFileGetContents`. In general, using `echo` is safe and necessary. However, `echo file_get_contents()` is potentially a bad thing. There is potential for several rules to be about using `echo` with something dangerous; adding `WithFileGetContents` makes it clear what's being tested. 28 | -------------------------------------------------------------------------------- /tests/RuleFactoryTest.php: -------------------------------------------------------------------------------- 1 | createRuleCollection(); 13 | 14 | $this->assertTrue( 15 | $rules->has('EvalFunction'), 16 | 'Collection should include the EvalFunction rule' 17 | ); 18 | 19 | $this->assertTrue( 20 | $rules->has('ExitOrDie'), 21 | 'Collection should include the ExitOrDie rule' 22 | ); 23 | } 24 | 25 | public function testIncludeFilter() 26 | { 27 | $rules = (new RuleFactory(['evalfunction']))->createRuleCollection(); 28 | 29 | $this->assertTrue( 30 | $rules->has('EvalFunction'), 31 | 'Filtered collection should include the EvalFunction rule' 32 | ); 33 | 34 | $this->assertFalse( 35 | $rules->has('ExitOrDie'), 36 | 'Filtered collection should NOT include the ExitOrDie rule' 37 | ); 38 | } 39 | 40 | public function testExcludeFilter() 41 | { 42 | $rules = (new RuleFactory([], ['evalfunction']))->createRuleCollection(); 43 | 44 | $this->assertFalse( 45 | $rules->has('EvalFunction'), 46 | 'Filtered collection should NOT include the EvalFunction rule' 47 | ); 48 | 49 | $this->assertTrue( 50 | $rules->has('ExitOrDie'), 51 | 'Filtered collection should include the ExitOrDie rule' 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Subscriber/Json.php: -------------------------------------------------------------------------------- 1 | $this->issues]); 36 | } 37 | 38 | /** 39 | * Write issue to document 40 | * 41 | * @param IssueEvent $event 42 | * @return void 43 | */ 44 | public function onFileIssue(IssueEvent $event) 45 | { 46 | $this->issues[] = [ 47 | 'type' => $event->getRule()->getName(), 48 | 'description' => $event->getRule()->getDescription(), 49 | 'file' => $event->getFile()->getPath(), 50 | 'line' => $event->getNode()->getLine(), 51 | 'source' => implode("\n", $event->getFile()->fetchNode($event->getNode())) 52 | ]; 53 | } 54 | 55 | /** 56 | * Write error to document 57 | * 58 | * @param ErrorEvent $event 59 | * @return void 60 | */ 61 | public function onFileError(ErrorEvent $event) 62 | { 63 | $this->issues['error'] = [ 64 | 'description' => $event->getMessage(), 65 | 'file' => $event->getFile()->getPath() 66 | ]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Subscriber/ConsoleDotsTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('write')->ordered()->once()->with("."); 15 | $output->shouldReceive('write')->ordered()->once()->with("E"); 16 | $output->shouldReceive('write')->ordered()->once()->with("\n"); 17 | $output->shouldReceive('write')->ordered()->once()->with("I"); 18 | 19 | $console = new ConsoleDots($output); 20 | $console->setLineLength(2); 21 | 22 | $console->onScanStart(); 23 | 24 | // Writes a dot as a file is scanned 25 | $console->onFileOpen(m::mock('\Psecio\Parse\Event\FileEvent')); 26 | $console->onFileClose(); 27 | 28 | // Writes an E as an error occurs 29 | // Also triggers a new line as the line witdth is set to 2 30 | $console->onFileOpen(m::mock('\Psecio\Parse\Event\FileEvent')); 31 | $console->onFileError(m::mock('\Psecio\Parse\Event\ErrorEvent')); 32 | $console->onFileClose(); 33 | 34 | // Writes an I as an issue occurs 35 | $console->onFileOpen(m::mock('\Psecio\Parse\Event\FileEvent')); 36 | $console->onFileIssue(m::mock('\Psecio\Parse\Event\IssueEvent')); 37 | $console->onFileClose(); 38 | 39 | // Writes nothing 40 | $console->onDebug(m::mock('\Psecio\Parse\Event\MessageEvent')); 41 | 42 | $console->onScanComplete(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Subscriber/ConsoleProgressBar.php: -------------------------------------------------------------------------------- 1 | progressBar = $progressBar; 35 | $this->progressBar->setFormat( 36 | $this->progressBar->getMaxSteps() ? self::FORMAT_STEPS_KNOWN : self::FORMAT_STEPS_UNKNOWN 37 | ); 38 | } 39 | 40 | /** 41 | * Reset progress bar on scan start 42 | * 43 | * @return void 44 | */ 45 | public function onScanStart() 46 | { 47 | $this->progressBar->start(); 48 | } 49 | 50 | /** 51 | * Finish progress bar on scan complete 52 | * 53 | * @return void 54 | */ 55 | public function onScanComplete() 56 | { 57 | $this->progressBar->finish(); 58 | } 59 | 60 | /** 61 | * Advance progress bar on file close 62 | * 63 | * @return void 64 | */ 65 | public function onFileClose() 66 | { 67 | $this->progressBar->advance(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Rule/DisplayErrors.php: -------------------------------------------------------------------------------- 1 | display_errors setting determines whether errors should be printed 12 | * to the screen as part of the output or if they should be hidden from the user. 13 | * 14 | * Displaying errors can be helpful during development, but should never be used 15 | * in production as it may leak valueable information to an attacker. 16 | * 17 | * To prevent accidentaly displaying errors it is recommended that you never use 18 | * ini_set() to manually enable reporting. 19 | * 20 | * Example of failing code 21 | * 22 | * 23 | * ini_set('display_errors', true); 24 | * 25 | * 26 | * How to fix? 27 | * 28 | * Configure display_errors in your php.ini file. And make sure that it is set to 29 | * false in production. 30 | */ 31 | class DisplayErrors implements RuleInterface 32 | { 33 | use Helper\NameTrait, Helper\DocblockDescriptionTrait, Helper\IsFunctionCallTrait, Helper\IsBoolLiteralTrait; 34 | 35 | /** 36 | * @var array List of allowed display_errors settings 37 | */ 38 | private $allowed = [0, '0', false, 'false', 'off', 'stderr']; 39 | 40 | public function isValid(Node $node) 41 | { 42 | if ($this->isFunctionCall($node, 'ini_set') && $this->readArgument($node, 0) === 'display_errors') { 43 | return in_array($this->readArgument($node, 1), $this->allowed, true); 44 | } 45 | return true; 46 | } 47 | 48 | private function readArgument(Node $node, $index) 49 | { 50 | $arg = $this->getCalledFunctionArgument($node, $index); 51 | if ($this->isBoolLiteral($arg->value)) { 52 | return (string)$arg->value->name; 53 | } 54 | return property_exists($arg->value, 'value') ? $arg->value->value : ''; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Rule/PregReplaceWithEvalModifier.php: -------------------------------------------------------------------------------- 1 | e modifier set preg_replace() does normal substitution of 12 | * backreferences in the replacement string, evaluates it as PHP code, and 13 | * uses the result for replacing the search string. 14 | * 15 | * This modifier is deprecated as of PHP 5.5 and use is highly discouraged 16 | * as it can easily introduce security vulnerabilites. 17 | * 18 | * Example of failing code 19 | * 20 | * The following code can be easily exploited by passing in a string such as 21 | *

{${eval($_GET[php_code])}}

. This gives the attacker the ability 22 | * to execute arbitrary PHP code and as such gives him nearly complete access 23 | * to your server. 24 | * 25 | * $html = preg_replace( 26 | * '((.*?))e', 27 | * '"" . strtoupper("$2") . ""', 28 | * $_POST['html'] 29 | * ); 30 | * 31 | * How to fix? 32 | * 33 | * Use the preg_replace_callback() function instead. 34 | * 35 | * $html = preg_replace_callback( 36 | * '((.*?))', 37 | * function ($m) { 38 | * return "" . strtoupper($m[2]) . ""; 39 | * }, 40 | * $_POST['html'] 41 | * ); 42 | */ 43 | class PregReplaceWithEvalModifier implements RuleInterface 44 | { 45 | use Helper\NameTrait, Helper\DocblockDescriptionTrait, Helper\IsFunctionCallTrait; 46 | 47 | public function isValid(Node $node) 48 | { 49 | if ($this->isFunctionCall($node, 'preg_replace')) { 50 | $value = $this->getCalledFunctionArgument($node, 0)->value; 51 | 52 | if (property_exists($value, 'value') && preg_match("/e[a-zA-Z]*$/", $value->value)) { 53 | return false; 54 | } 55 | } 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Fakes/FakeDocComment.php: -------------------------------------------------------------------------------- 1 | disabledRules = $disabled; 20 | $this->enabledRules = $enabled; 21 | } 22 | 23 | /** 24 | * Get doc comment summary 25 | * 26 | * @return string 27 | */ 28 | public function getSummary() 29 | { 30 | return ''; 31 | } 32 | 33 | /** 34 | * Get doc block body 35 | * 36 | * @return string 37 | */ 38 | public function getBody() 39 | { 40 | return ''; 41 | } 42 | 43 | /** 44 | * Get defined tags 45 | * 46 | * @return array 47 | */ 48 | public function getTags() 49 | { 50 | return []; 51 | } 52 | 53 | /** 54 | * Get tag values matching $tagName 55 | * 56 | * @param string $tagName 57 | * 58 | * @return array List of matching values 59 | */ 60 | public function getMatchingTags($tagName) 61 | { 62 | return []; 63 | } 64 | 65 | /** 66 | * Get tag values matching $tagName, case insensitively 67 | * 68 | * @param string $tagName 69 | * 70 | * @return array List of matching values 71 | */ 72 | public function getIMatchingTags($tagName) 73 | { 74 | if (strtolower($tagName) == 'psecio\parse\disable') { 75 | return $this->disabledRules; 76 | } 77 | 78 | if (strtolower($tagName) == 'psecio\parse\enable') { 79 | return $this->enabledRules; 80 | } 81 | 82 | return []; 83 | } 84 | 85 | /** 86 | * Get the original, raw comment 87 | * 88 | * @return string 89 | */ 90 | public function getRawComment() 91 | { 92 | return ''; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Subscriber/XmlTest.php: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | error description 15 | /error/path 16 | 17 | 18 | RuleName 19 | issue description 20 | /issue/path 21 | 1 22 | 23 | 24 | 25 | "; 26 | 27 | $output = m::mock('\Symfony\Component\Console\Output\OutputInterface'); 28 | 29 | $output->shouldReceive('writeln')->once()->with( 30 | $xmlStr, 31 | \Symfony\Component\Console\Output\OutputInterface::OUTPUT_RAW 32 | ); 33 | 34 | $xml = new Xml($output); 35 | 36 | $xml->onScanStart(); 37 | 38 | $errorEvent = m::mock('\Psecio\Parse\Event\ErrorEvent'); 39 | $errorEvent->shouldReceive('getMessage')->once()->andReturn('error description'); 40 | $errorEvent->shouldReceive('getFile->getPath')->once()->andReturn('/error/path'); 41 | 42 | $xml->onFileError($errorEvent); 43 | 44 | $file = m::mock('\Psecio\Parse\File'); 45 | $file->shouldReceive('getPath')->once()->andReturn('/issue/path'); 46 | $file->shouldReceive('fetchNode')->once()->andReturn(['php source']); 47 | 48 | $issueEvent = m::mock('\Psecio\Parse\Event\IssueEvent'); 49 | $issueEvent->shouldReceive('getNode')->atLeast(1)->andReturn( 50 | m::mock('PhpParser\Node')->shouldReceive('getLine')->atLeast(1)->andReturn(1)->mock() 51 | ); 52 | $issueEvent->shouldReceive('getRule->getName')->once()->andReturn('RuleName'); 53 | $issueEvent->shouldReceive('getRule->getDescription')->once()->andReturn('issue description'); 54 | $issueEvent->shouldReceive('getFile')->zeroOrMoreTimes()->andReturn($file); 55 | 56 | $xml->onFileIssue($issueEvent); 57 | 58 | $xml->onScanComplete(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Subscriber/ConsoleDots.php: -------------------------------------------------------------------------------- 1 | lineLength = $lineLength; 40 | } 41 | 42 | /** 43 | * Write header on scan start 44 | * 45 | * @return void 46 | */ 47 | public function onScanStart() 48 | { 49 | $this->fileCount = 0; 50 | } 51 | 52 | /** 53 | * Set status to valid on file open 54 | * 55 | * @param FileEvent $event 56 | * @return void 57 | */ 58 | public function onFileOpen(FileEvent $event) 59 | { 60 | $this->fileCount++; 61 | $this->status = '.'; 62 | } 63 | 64 | /** 65 | * Write file status on file close 66 | * 67 | * @return void 68 | */ 69 | public function onFileClose() 70 | { 71 | $this->write($this->status); 72 | if ($this->fileCount % $this->lineLength == 0) { 73 | $this->write("\n"); 74 | } 75 | } 76 | 77 | /** 78 | * Set file status to I on file issue 79 | * 80 | * @param IssueEvent $event 81 | * @return void 82 | */ 83 | public function onFileIssue(IssueEvent $event) 84 | { 85 | $this->status = 'I'; 86 | } 87 | 88 | /** 89 | * Set file status to E on file error 90 | * 91 | * @param ErrorEvent $event 92 | * @return void 93 | */ 94 | public function onFileError(ErrorEvent $event) 95 | { 96 | $this->status = 'E'; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Subscriber/ConsoleLinesTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('write')->ordered()->once()->with("[PARSE] /path/to/file\n"); 15 | $output->shouldReceive('write')->ordered()->once()->with("[PARSE] /path/to/file\n"); 16 | $output->shouldReceive('write')->ordered()->once()->with("[ERROR] message in /path/to/file\n"); 17 | $output->shouldReceive('write')->ordered()->once()->with("[PARSE] /path/to/file\n"); 18 | $output->shouldReceive('write')->ordered()->once()->with("[ISSUE] [Rule] On line 1 in path\n"); 19 | 20 | // Data for [PARSE] lines 21 | $fileEvent = m::mock('\Psecio\Parse\Event\FileEvent'); 22 | $fileEvent->shouldReceive('getFile->getPath')->andReturn('/path/to/file'); 23 | 24 | // Data for [ERROR] line 25 | $errorEvent = m::mock('\Psecio\Parse\Event\ErrorEvent'); 26 | $errorEvent->shouldReceive('getMessage')->andReturn('message'); 27 | $errorEvent->shouldReceive('getFile->getPath')->andReturn('/path/to/file'); 28 | 29 | // Data for [ISSUE] line 30 | $issueEvent = m::mock('\Psecio\Parse\Event\IssueEvent'); 31 | $issueEvent->shouldReceive('getNode->getLine')->andReturn(1); 32 | $issueEvent->shouldReceive('getRule->getName')->andReturn('Rule'); 33 | $issueEvent->shouldReceive('getFile->getPath')->andReturn('path'); 34 | 35 | $console = new ConsoleLines($output); 36 | 37 | $console->onScanStart(); 38 | 39 | // File open writes [PARSE] line 40 | $console->onFileOpen($fileEvent); 41 | $console->onFileClose(); 42 | 43 | // Writes [PARSE] and [ERROR] lines 44 | $console->onFileOpen($fileEvent); 45 | $console->onFileError($errorEvent); 46 | $console->onFileClose(); 47 | 48 | // Writes [PARSE] and [ISSUE] lines 49 | $console->onFileOpen($fileEvent); 50 | $console->onFileIssue($issueEvent); 51 | $console->onFileClose(); 52 | 53 | // Writes nothing 54 | $console->onDebug(m::mock('\Psecio\Parse\Event\MessageEvent')); 55 | 56 | $console->onScanComplete(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/RuleFactory.php: -------------------------------------------------------------------------------- 1 | collection = new RuleCollection; 26 | $this->setUpCollection(); 27 | if ($blacklist) { 28 | $this->blacklist($blacklist); 29 | } 30 | if ($whitelist) { 31 | $this->whitelist($whitelist); 32 | } 33 | } 34 | 35 | /** 36 | * Get collection of rules 37 | * 38 | * @return RuleCollection 39 | */ 40 | public function createRuleCollection() 41 | { 42 | return $this->collection; 43 | } 44 | 45 | /** 46 | * Remove blacklisted rules from collection 47 | * 48 | * @param string[] $names Names of rules to remove 49 | * @return void 50 | */ 51 | public function blacklist(array $names) 52 | { 53 | foreach ($names as $ruleName) { 54 | $this->collection->remove($ruleName); 55 | } 56 | } 57 | 58 | /** 59 | * Remove non-whitelisted rules from collection 60 | * 61 | * @param string[] $names Names of rules to keep 62 | * @return void 63 | */ 64 | public function whitelist(array $names) 65 | { 66 | $oldCollection = $this->collection; 67 | $this->collection = new RuleCollection; 68 | foreach ($names as $ruleName) { 69 | $this->collection->add( 70 | $oldCollection->get($ruleName) 71 | ); 72 | } 73 | } 74 | 75 | /** 76 | * Fill $collection with bundled rules 77 | * 78 | * @return void 79 | */ 80 | private function setUpCollection() 81 | { 82 | foreach (new DirectoryIterator(__DIR__ . '/Rule') as $splFileInfo) { 83 | if ($splFileInfo->isDot() || $splFileInfo->isDir()) { 84 | continue; 85 | } 86 | $className = "\\Psecio\\Parse\\Rule\\{$splFileInfo->getBasename('.php')}"; 87 | $this->collection->add(new $className); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/FileTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException('RuntimeException'); 13 | new File(new SplFileInfo('this/really/does/not/exist/at/all')); 14 | } 15 | 16 | public function testGetPath() 17 | { 18 | $this->assertEquals( 19 | __FILE__, 20 | (new File(new SplFileInfo(__FILE__)))->getPath(), 21 | 'The correct path should be returned' 22 | ); 23 | } 24 | 25 | public function testIsPathMatch() 26 | { 27 | $this->assertTrue( 28 | (new File(new SplFileInfo(__FILE__)))->isPathMatch('/.php$/'), 29 | 'Test should pass as the path of __FILE__ ends with .php' 30 | ); 31 | } 32 | 33 | public function testGetContent() 34 | { 35 | $this->assertRegExp( 36 | '/public function testGetContent()/', 37 | (new File(new SplFileInfo(__FILE__)))->getContents(), 38 | 'The contents from this file should be fetched correctly' 39 | ); 40 | } 41 | 42 | public function testFetchLines() 43 | { 44 | $filename = tempnam(sys_get_temp_dir(), 'psecio-parse-'); 45 | file_put_contents($filename, "line 1\nline 2\nline 3\nline 4"); 46 | 47 | $file = new File(new SplFileInfo($filename)); 48 | 49 | $this->assertSame( 50 | ["line 2"], 51 | $file->fetchLines(2), 52 | 'A single argument to fetchLines should fetch only one line' 53 | ); 54 | 55 | $this->assertSame( 56 | ["line 2", "line 3", "line 4"], 57 | $file->fetchLines(2, 4), 58 | 'Two arguments to fetchLines should fetch the complete series of lines' 59 | ); 60 | 61 | $this->assertSame( 62 | ["line 3"], 63 | $file->fetchLines(3, 3), 64 | 'Specifying the same line twice should grab that line' 65 | ); 66 | 67 | $this->assertSame( 68 | ["line 1", "line 2"], 69 | $file->fetchNode( 70 | m::mock('PhpParser\Node') 71 | ->shouldReceive('getAttributes') 72 | ->once() 73 | ->andReturn(['startLine' => 1, 'endLine' => 2]) 74 | ->mock() 75 | ), 76 | 'fetchNode should fetch based on node attributes' 77 | ); 78 | 79 | unlink($filename); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Subscriber/Xml.php: -------------------------------------------------------------------------------- 1 | xmlWriter = new XMLWriter; 30 | $this->xmlWriter->openMemory(); 31 | $this->xmlWriter->startDocument('1.0', 'UTF-8'); 32 | $this->xmlWriter->setIndent(true); 33 | $this->xmlWriter->startElement('results'); 34 | } 35 | 36 | /** 37 | * Output document at scan complete 38 | * 39 | * @return void 40 | */ 41 | public function onScanComplete() 42 | { 43 | $this->xmlWriter->endElement(); 44 | $this->xmlWriter->endDocument(); 45 | $this->output->writeln( 46 | $this->xmlWriter->flush(), 47 | OutputInterface::OUTPUT_RAW 48 | ); 49 | } 50 | 51 | /** 52 | * Write issue to document 53 | * 54 | * @param IssueEvent $event 55 | * @return void 56 | */ 57 | public function onFileIssue(IssueEvent $event) 58 | { 59 | $this->xmlWriter->startElement('issue'); 60 | $this->xmlWriter->writeElement('type', $event->getRule()->getName()); 61 | $this->xmlWriter->writeElement('description', $event->getRule()->getDescription()); 62 | $this->xmlWriter->writeElement('file', $event->getFile()->getPath()); 63 | $this->xmlWriter->writeElement('line', $event->getNode()->getLine()); 64 | $this->xmlWriter->startElement('source'); 65 | $this->xmlWriter->writeCData(implode("\n", $event->getFile()->fetchNode($event->getNode()))); 66 | $this->xmlWriter->endElement(); 67 | $this->xmlWriter->endElement(); 68 | } 69 | 70 | /** 71 | * Write error to document 72 | * 73 | * @param ErrorEvent $event 74 | * @return void 75 | */ 76 | public function onFileError(ErrorEvent $event) 77 | { 78 | $this->xmlWriter->startElement('error'); 79 | $this->xmlWriter->writeElement('description', $event->getMessage()); 80 | $this->xmlWriter->writeElement('file', $event->getFile()->getPath()); 81 | $this->xmlWriter->endElement(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Subscriber/Subscriber.php: -------------------------------------------------------------------------------- 1 | 'onScanStart', 28 | self::SCAN_COMPLETE => 'onScanComplete', 29 | self::FILE_OPEN => 'onFileOpen', 30 | self::FILE_CLOSE => 'onFileClose', 31 | self::FILE_ISSUE => 'onFileIssue', 32 | self::FILE_ERROR => 'onFileError', 33 | self::DEBUG => 'onDebug' 34 | ]; 35 | } 36 | 37 | /** 38 | * Empty on scan start method 39 | * 40 | * @return void 41 | */ 42 | public function onScanStart() 43 | { 44 | } 45 | 46 | /** 47 | * Empty on scan complete method 48 | * 49 | * @return void 50 | */ 51 | public function onScanComplete() 52 | { 53 | } 54 | 55 | /** 56 | * Empty on file open method 57 | * 58 | * @param FileEvent $event 59 | * @return void 60 | */ 61 | public function onFileOpen(FileEvent $event) 62 | { 63 | } 64 | 65 | /** 66 | * Empty on file close method 67 | * 68 | * @return void 69 | */ 70 | public function onFileClose() 71 | { 72 | } 73 | 74 | /** 75 | * Empty on file issue method 76 | * 77 | * @param IssueEvent $event 78 | * @return void 79 | */ 80 | public function onFileIssue(IssueEvent $event) 81 | { 82 | } 83 | 84 | /** 85 | * Empty on file error method 86 | * 87 | * @param ErrorEvent $event 88 | * @return void 89 | */ 90 | public function onFileError(ErrorEvent $event) 91 | { 92 | } 93 | 94 | /** 95 | * Empty on debug method 96 | * 97 | * @param MessageEvent $event 98 | * @return void 99 | */ 100 | public function onDebug(MessageEvent $event) 101 | { 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/File.php: -------------------------------------------------------------------------------- 1 | isReadable()) { 33 | throw new RuntimeException("Failed to open file '{$splFileInfo->getRealPath()}'"); 34 | } 35 | $this->splFileInfo = $splFileInfo; 36 | $this->lines = file($splFileInfo->getRealPath(), FILE_IGNORE_NEW_LINES); 37 | } 38 | 39 | /** 40 | * Get to SplFileInfo object 41 | * 42 | * @return SplFileInfo 43 | */ 44 | public function getSplFileInfo() 45 | { 46 | return $this->splFileInfo; 47 | } 48 | 49 | /** 50 | * Get the file path 51 | * 52 | * @return string 53 | */ 54 | public function getPath() 55 | { 56 | return $this->getSplFileInfo()->getPathname(); 57 | } 58 | 59 | /** 60 | * Test if path matches a regular expression 61 | * 62 | * @param string $regexp 63 | * @return bool 64 | */ 65 | public function isPathMatch($regexp) 66 | { 67 | return !!preg_match($regexp, $this->getPath()); 68 | } 69 | 70 | /** 71 | * Get the file contents 72 | * 73 | * @return string File contents 74 | */ 75 | public function getContents() 76 | { 77 | return implode("\n", $this->lines); 78 | } 79 | 80 | /** 81 | * Pull out given lines from file contents 82 | * 83 | * @param integer $startLine 84 | * @param integer $endLine 85 | * @return string[] 86 | */ 87 | public function fetchLines($startLine, $endLine = 0) 88 | { 89 | $startLine--; 90 | $endLine = $endLine ?: $startLine; 91 | $length = $endLine - $startLine; 92 | $length = $length ?: 1; 93 | 94 | return array_slice($this->lines, $startLine, $length); 95 | } 96 | 97 | /** 98 | * Fetch Node line contents 99 | * 100 | * @param Node $node 101 | * @return string[] 102 | */ 103 | public function fetchNode(Node $node) 104 | { 105 | $attr = $node->getAttributes(); 106 | return $this->fetchLines($attr['startLine'], $attr['endLine']); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Rule/HardcodedSensitiveValues.php: -------------------------------------------------------------------------------- 1 | getNameAndValue($node); 27 | if ($name === false) { 28 | return true; 29 | } 30 | 31 | // Fail on straight $var = 'value', where $var is in $sensitiveNames 32 | return !($this->isSensitiveName($name) && 33 | $value instanceof \PhpParser\Node\Scalar\String_); 34 | } 35 | 36 | protected function getNameAndValue($node) 37 | { 38 | if ($this->isExpression($node, 'Assign') && property_exists($node->var, 'name')) { 39 | return [$node->var->name, $node->expr]; 40 | } 41 | 42 | if ($node instanceof \PhpParser\Node\Const_) { 43 | return [$node->name, $node->value]; 44 | } 45 | 46 | if ($this->isFunctionCall($node, 'define')) { 47 | $constantNameArgument = $this->getCalledFunctionArgument($node, 0)->value; 48 | 49 | if (property_exists($constantNameArgument, 'value')) { 50 | $name = $constantNameArgument->value; 51 | $value = $this->getCalledFunctionArgument($node, 1)->value; 52 | 53 | return [$name, $value]; 54 | } 55 | } 56 | 57 | return [false, false]; 58 | } 59 | 60 | public function isSensitiveName($name) 61 | { 62 | if (!is_string($name)) { 63 | return false; 64 | } 65 | $name = strtolower($name); 66 | return $this->matchSearchList($name, $this->sensitiveNames); 67 | } 68 | 69 | protected function matchSearchList($name, $list) 70 | { 71 | foreach ($list as $match) { 72 | if ($this->startsWith($name, $match) || $this->endsWith($name, $match)) { 73 | return true; 74 | } 75 | } 76 | } 77 | 78 | protected function startsWith($haystack, $needle) 79 | { 80 | return strpos($haystack, $needle) === 0; 81 | } 82 | 83 | protected function endsWith($haystack, $needle) 84 | { 85 | return strrpos($haystack, $needle) === (strlen($haystack) - strlen($needle)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Subscriber/ConsoleReportTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('writeln') 14 | ->once() 15 | ->with("\n\nOK (2 files scanned)") 16 | ->mock() 17 | ); 18 | 19 | $report->onScanStart(); 20 | $report->onFileOpen(m::mock('\Psecio\Parse\Event\FileEvent')); 21 | $report->onFileOpen(m::mock('\Psecio\Parse\Event\FileEvent')); 22 | $report->onScanComplete(); 23 | } 24 | 25 | public function testFailureReport() 26 | { 27 | $expected = " 28 | 29 | There was 1 error 30 | 31 | 1) /error/path 32 | error description 33 | 34 | There was 1 issue 35 | 36 | 1) /issue/path on line 1 37 | issue description 38 | > php source 39 | For more information execute 'psecio-parse rules rulename' 40 | 41 | FAILURES! 42 | Scanned: 0, Errors: 1, Issues: 1."; 43 | 44 | $report = new ConsoleReport( 45 | m::mock('\Symfony\Component\Console\Output\OutputInterface') 46 | ->shouldReceive('writeln') 47 | ->once() 48 | ->with($expected) 49 | ->mock() 50 | ); 51 | 52 | $report->onScanStart(); 53 | 54 | $errorEvent = m::mock('\Psecio\Parse\Event\ErrorEvent'); 55 | $errorEvent->shouldReceive('getMessage')->once()->andReturn('error description'); 56 | $errorEvent->shouldReceive('getFile->getPath')->once()->andReturn('/error/path'); 57 | 58 | $report->onFileError($errorEvent); 59 | 60 | $file = m::mock('\Psecio\Parse\File'); 61 | $file->shouldReceive('getPath')->once()->andReturn('/issue/path'); 62 | $file->shouldReceive('fetchNode')->once()->andReturn(['php source']); 63 | 64 | $issueEvent = m::mock('\Psecio\Parse\Event\IssueEvent'); 65 | $issueEvent->shouldReceive('getNode')->atLeast(1)->andReturn( 66 | m::mock('PhpParser\Node')->shouldReceive('getLine')->atLeast(1)->andReturn(1)->mock() 67 | ); 68 | $issueEvent->shouldReceive('getRule')->atLeast(1)->andReturn( 69 | m::mock('\Psecio\Parse\RuleInterface') 70 | ->shouldReceive('getDescription')->once()->andReturn('issue description') 71 | ->shouldReceive('getName')->once()->andReturn('rulename') 72 | ->mock() 73 | ); 74 | 75 | $issueEvent->shouldReceive('getFile')->zeroOrMoreTimes()->andReturn($file); 76 | 77 | $report->onFileIssue($issueEvent); 78 | 79 | $report->onScanComplete(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/RuleCollection.php: -------------------------------------------------------------------------------- 1 | add($rule); 29 | } 30 | } 31 | 32 | /** 33 | * Return a count of the current ruleset 34 | * 35 | * @return integer Count result 36 | */ 37 | public function count() 38 | { 39 | return count($this->rules); 40 | } 41 | 42 | /** 43 | * Get iterator for ruleset 44 | * 45 | * @return ArrayIterator 46 | */ 47 | public function getIterator() 48 | { 49 | return new ArrayIterator($this->rules); 50 | } 51 | 52 | /** 53 | * Add an rule to collection 54 | * 55 | * @param RuleInterface $rule 56 | * @return void 57 | */ 58 | public function add(RuleInterface $rule) 59 | { 60 | $this->rules[strtolower($rule->getName())] = $rule; 61 | } 62 | 63 | /** 64 | * Check if rule exist in collection 65 | * 66 | * @param string $name Name of rule 67 | * @return bool 68 | */ 69 | public function has($name) 70 | { 71 | return array_key_exists(strtolower($name), $this->rules); 72 | } 73 | 74 | /** 75 | * Get rule from collection 76 | * 77 | * @param string $name Name of rule 78 | * @return RuleInterface 79 | * @throws RuntimeException If rule does not exist 80 | */ 81 | public function get($name) 82 | { 83 | if ($this->has($name)) { 84 | return $this->rules[strtolower($name)]; 85 | } 86 | throw new RuntimeException("The rule $name does not exist"); 87 | } 88 | 89 | /** 90 | * Remove an item from the collection 91 | * 92 | * @param string $name Name of rule 93 | * @return void 94 | * @throws RuntimeException If rule does not exist 95 | */ 96 | public function remove($name) 97 | { 98 | if ($this->has($name)) { 99 | unset($this->rules[strtolower($name)]); 100 | return; 101 | } 102 | throw new RuntimeException("The rule $name does not exist"); 103 | } 104 | 105 | /** 106 | * Return the collection an array 107 | * 108 | * @return array Current data 109 | */ 110 | public function toArray() 111 | { 112 | return $this->rules; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Rule/Helper/IsFunctionCallTrait.php: -------------------------------------------------------------------------------- 1 | getCalledFunctionName($node)), $names, true); 32 | } 33 | return true; 34 | } 35 | return false; 36 | } 37 | 38 | /** 39 | * Get name of called function 40 | * 41 | * @param Node $node 42 | * @return string Empty string if name could not be parsed 43 | * @throws LogicException If node is not an instance of FuncCall 44 | */ 45 | protected function getCalledFunctionName(Node $node) 46 | { 47 | if (!$node instanceof FuncCall) { 48 | throw new LogicException('Node must be an instance of FuncCall, found: ' . get_class($node)); 49 | } 50 | if ($node->name instanceof Name) { 51 | return (string)$node->name; 52 | } 53 | return ''; 54 | } 55 | 56 | /** 57 | * Get argument of called function 58 | * 59 | * @param Node $node 60 | * @param integer $index Index of argument to fetch 61 | * @return Arg If argument is not found an empty string is returned 62 | * @throws LogicException If node is not an instance of FuncCall 63 | */ 64 | protected function getCalledFunctionArgument(Node $node, $index) 65 | { 66 | if (!$node instanceof FuncCall) { 67 | throw new LogicException('Node must be an instance of FuncCall, found: ' . get_class($node)); 68 | } 69 | if (is_array($node->args) && array_key_exists($index, $node->args)) { 70 | return $node->args[$index]; 71 | } 72 | return new Arg(new SString('')); 73 | } 74 | 75 | /** 76 | * Count the arguments of called function 77 | * 78 | * @param Node $node 79 | * @return integer 80 | * @throws LogicException If node is not an instance of FuncCall 81 | */ 82 | protected function countCalledFunctionArguments(Node $node) 83 | { 84 | if (!$node instanceof FuncCall) { 85 | throw new LogicException('Node must be an instance of FuncCall, found: ' . get_class($node)); 86 | } 87 | if (is_array($node->args)) { 88 | return count($node->args); 89 | } 90 | return 0; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/Fakes/FakeNode.php: -------------------------------------------------------------------------------- 1 | setDocComment($docComment); 20 | } 21 | 22 | /** 23 | * For the fake, set the doc comment of this node 24 | * 25 | * @param string $docComment The comment to use 26 | */ 27 | public function setDocComment($docComment) 28 | { 29 | $this->docComment = $docComment; 30 | } 31 | 32 | /** 33 | * Gets the type of the node. 34 | * 35 | * @return string Type of the node 36 | */ 37 | public function getType() 38 | { 39 | return 'Node'; 40 | } 41 | 42 | /** 43 | * Gets the names of the sub nodes. 44 | * 45 | * @return array Names of sub nodes 46 | */ 47 | public function getSubNodeNames() 48 | { 49 | return []; 50 | } 51 | 52 | /** 53 | * Gets line the node started in. 54 | * 55 | * @return int Line 56 | */ 57 | public function getLine() 58 | { 59 | return 0; 60 | } 61 | 62 | /** 63 | * Sets line the node started in. 64 | * 65 | * @param int $line Line 66 | */ 67 | public function setLine($line) 68 | { 69 | } 70 | 71 | /** 72 | * Gets the doc comment of the node. 73 | * 74 | * The doc comment has to be the last comment associated with the node. 75 | * 76 | * @return null|string|Comment\Doc Doc comment object or null 77 | */ 78 | public function getDocComment() 79 | { 80 | return $this->docComment; 81 | } 82 | 83 | /** 84 | * Sets an attribute on a node. 85 | * 86 | * @param string $key 87 | * @param mixed $value 88 | */ 89 | public function setAttribute($key, $value) 90 | { 91 | $this->attributes[$key] = $value; 92 | } 93 | 94 | /** 95 | * Returns whether an attribute exists. 96 | * 97 | * @param string $key 98 | * 99 | * @return bool 100 | */ 101 | public function hasAttribute($key) 102 | { 103 | return isset($this->attributes[$key]); 104 | } 105 | 106 | /** 107 | * Returns the value of an attribute. 108 | * 109 | * @param string $key 110 | * @param mixed $default 111 | * 112 | * @return mixed 113 | */ 114 | public function &getAttribute($key, $default = null) 115 | { 116 | $r =& $default; 117 | if (isset($this->attributes[$key])) { 118 | $r =& $this->attributes[$key]; 119 | } 120 | 121 | return $r; 122 | } 123 | 124 | /** 125 | * Returns all attributes for the given node. 126 | * 127 | * @return array 128 | */ 129 | public function getAttributes() 130 | { 131 | return $this->attributes; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Scanner.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 54 | $this->visitor = $visitor; 55 | $this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); 56 | $this->traverser = $traverser ?: new NodeTraverser; 57 | $this->visitor->onNodeFailure([$this, 'onNodeFailure']); 58 | $this->traverser->addVisitor($this->visitor); 59 | } 60 | 61 | /** 62 | * Node fail callback 63 | * 64 | * @param RuleInterface $rule 65 | * @param Node $node 66 | * @param File $file 67 | * @return void 68 | */ 69 | public function onNodeFailure(RuleInterface $rule, Node $node, File $file) 70 | { 71 | $this->dispatcher->dispatch(self::FILE_ISSUE, new Event\IssueEvent($rule, $node, $file)); 72 | } 73 | 74 | /** 75 | * Execute the scan 76 | * 77 | * @param FileIterator $fileIterator Iterator with files to scan 78 | * @return void 79 | */ 80 | public function scan(FileIterator $fileIterator) 81 | { 82 | $this->dispatcher->dispatch(self::SCAN_START); 83 | 84 | foreach ($fileIterator as $file) { 85 | $this->dispatcher->dispatch(self::FILE_OPEN, new Event\FileEvent($file)); 86 | 87 | if ($file->isPathMatch('/\.phps$/i')) { 88 | $this->dispatcher->dispatch( 89 | self::FILE_ERROR, 90 | new Event\ErrorEvent('You have a .phps file - REMOVE NOW', $file) 91 | ); 92 | } 93 | 94 | try { 95 | $this->visitor->setFile($file); 96 | $this->traverser->traverse($this->parser->parse($file->getContents())); 97 | } catch (\PhpParser\Error $e) { 98 | $this->dispatcher->dispatch( 99 | self::FILE_ERROR, 100 | new Event\ErrorEvent($e->getMessage(), $file) 101 | ); 102 | } 103 | 104 | $this->dispatcher->dispatch(self::FILE_CLOSE); 105 | } 106 | 107 | $this->dispatcher->dispatch(self::SCAN_COMPLETE); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/RuleCollectionTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getName') 16 | ->andReturn('name') 17 | ->mock(); 18 | 19 | $collection = new RuleCollection; 20 | 21 | $this->assertCount( 22 | 0, 23 | $collection, 24 | 'An empty collection should count to 0' 25 | ); 26 | 27 | $this->assertFalse( 28 | $collection->has('name'), 29 | 'Collection should not contain the name rule' 30 | ); 31 | 32 | $collection->add($rule); 33 | 34 | $this->assertCount( 35 | 1, 36 | $collection, 37 | '1 added item should be reflected in count' 38 | ); 39 | 40 | $this->assertTrue( 41 | $collection->has('name'), 42 | 'The name rule was added and should be contained' 43 | ); 44 | 45 | $this->assertSame( 46 | $rule, 47 | $collection->get('name'), 48 | 'The named rule should be returned' 49 | ); 50 | 51 | $collection->remove('name'); 52 | 53 | $this->assertCount( 54 | 0, 55 | $collection, 56 | '1 removed item should be reflected in count' 57 | ); 58 | 59 | $this->assertFalse( 60 | $collection->has('name'), 61 | 'Rule was removed and should not be contained' 62 | ); 63 | } 64 | 65 | public function testCaseInsensitivity() 66 | { 67 | $collection = new RuleCollection; 68 | 69 | $collection->add( 70 | m::mock('\Psecio\Parse\RuleInterface') 71 | ->shouldReceive('getName') 72 | ->andReturn('name') 73 | ->mock() 74 | ); 75 | 76 | $this->assertTrue( 77 | $collection->has('NAME'), 78 | 'name should be accessible independent of case' 79 | ); 80 | 81 | $this->assertSame( 82 | $collection->get('NAme'), 83 | $collection->get('naME'), 84 | 'getting a rule should be case insensitive' 85 | ); 86 | } 87 | 88 | public function testIteratorAndArray() 89 | { 90 | $rule = m::mock('\Psecio\Parse\RuleInterface') 91 | ->shouldReceive('getName') 92 | ->andReturn('name') 93 | ->mock(); 94 | 95 | $this->assertSame( 96 | iterator_to_array(new RuleCollection([$rule])), 97 | ['name' => $rule], 98 | 'iteration of the collection should work correctly' 99 | ); 100 | 101 | $this->assertSame( 102 | (new RuleCollection([$rule]))->toArray(), 103 | ['name' => $rule], 104 | 'toArray() should return the correct array' 105 | ); 106 | } 107 | 108 | public function testExceptionInGet() 109 | { 110 | $this->setExpectedException('RuntimeException'); 111 | (new RuleCollection)->get('does-not-exist'); 112 | } 113 | 114 | public function testExceptionInRemove() 115 | { 116 | $this->setExpectedException('RuntimeException'); 117 | (new RuleCollection)->remove('does-not-exist'); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Command/RulesCommand.php: -------------------------------------------------------------------------------- 1 | setName('rules') 26 | ->setDescription('Display information about the pesecio-parse ruleset') 27 | ->addArgument( 28 | 'rule', 29 | InputArgument::OPTIONAL, 30 | 'Display info about rule' 31 | ) 32 | ->setHelp( 33 | "Display info about rules:\n\n psecio-parse %command.name% [name-of-rule]\n" 34 | ); 35 | } 36 | 37 | /** 38 | * Execute the "rules" command 39 | * 40 | * @param InputInterface $input Input object 41 | * @param OutputInterface $output Output object 42 | * @return void 43 | */ 44 | public function execute(InputInterface $input, OutputInterface $output) 45 | { 46 | $rules = (new RuleFactory)->createRuleCollection(); 47 | 48 | if ($rulename = $input->getArgument('rule')) { 49 | $this->describeRule($rules->get($rulename), $output); 50 | return; 51 | } 52 | 53 | $this->listRules($rules, $output); 54 | } 55 | 56 | /** 57 | * List all bundled rules 58 | * 59 | * @param RuleCollection $rules 60 | * @param OutputInterface $output 61 | * @return void 62 | */ 63 | public function listRules(RuleCollection $rules, OutputInterface $output) 64 | { 65 | $table = new Table($output); 66 | $table->setStyle('compact'); 67 | $table->setHeaders(['Name', 'Description']); 68 | 69 | foreach ($rules as $rule) { 70 | $table->addRow( 71 | [ 72 | ''.$rule->getName().'', 73 | $rule->getDescription() 74 | ] 75 | ); 76 | } 77 | 78 | $table->render(); 79 | $output->writeln("\n Use 'psecio-parse rules name-of-rule' for more info about a specific rule"); 80 | } 81 | 82 | /** 83 | * Display rule description 84 | * 85 | * @param RuleInterface $rule 86 | * @param OutputInterface $output 87 | * @return void 88 | */ 89 | public function describeRule(RuleInterface $rule, OutputInterface $output) 90 | { 91 | $output->getFormatter()->setStyle( 92 | 'strong', 93 | new OutputFormatterStyle(null, null, ['bold', 'reverse']) 94 | ); 95 | $output->getFormatter()->setStyle( 96 | 'em', 97 | new OutputFormatterStyle('yellow', null, ['bold']) 98 | ); 99 | $output->getFormatter()->setStyle( 100 | 'code', 101 | new OutputFormatterStyle('green') 102 | ); 103 | $output->writeln("{$rule->getName()}\n"); 104 | $output->writeln("{$rule->getLongDescription()}\n"); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Command/ScanCommandTest.php: -------------------------------------------------------------------------------- 1 | assertRegExp( 33 | '/\./', 34 | $this->executeCommand(['--format' => 'dots']), 35 | 'Using --format=dots should generate output' 36 | ); 37 | } 38 | 39 | public function testProgressOutput() 40 | { 41 | $this->assertRegExp( 42 | '/\[\=+\]/', 43 | $this->executeCommand(['--format' => 'progress'], ['decorated' => true]), 44 | 'Using --format=progress should use the progressbar' 45 | ); 46 | } 47 | 48 | public function testVerboseOutput() 49 | { 50 | $this->assertRegExp( 51 | '/\[PARSE\]/', 52 | $this->executeCommand([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]), 53 | 'Using -v should generate verbose output' 54 | ); 55 | } 56 | 57 | public function testVeryVerboseOutput() 58 | { 59 | $this->assertRegExp( 60 | '/\[DEBUG\]/', 61 | $this->executeCommand([], ['verbosity' => OutputInterface::VERBOSITY_VERY_VERBOSE]), 62 | 'Using -vv should generate debug output' 63 | ); 64 | } 65 | 66 | public function testXmlOutput() 67 | { 68 | $this->assertRegExp( 69 | '/^<\?xml version="1.0" encoding="UTF-8"\?>/', 70 | $this->executeCommand(['--format' => 'xml']), 71 | 'Using --format=xml should generate a valid xml doctype' 72 | ); 73 | } 74 | 75 | public function testExceptionOnUnknownFormat() 76 | { 77 | $this->setExpectedException('RuntimeException'); 78 | $this->executeCommand(['--format' => 'this-format-does-not-exist']); 79 | } 80 | 81 | public function testParseCsv() 82 | { 83 | $this->assertSame( 84 | ['php', 'phps'], 85 | (new ScanCommand)->parseCsv('php,phps'), 86 | 'parsing comma separated values should work' 87 | ); 88 | $this->assertSame( 89 | ['php', 'phps'], 90 | array_values((new ScanCommand)->parseCsv('php,,phps')), 91 | 'multiple commas should be skipped while parsing csv' 92 | ); 93 | $this->assertSame( 94 | [], 95 | (new ScanCommand)->parseCsv(''), 96 | 'parsing an empty string should return an empty array' 97 | ); 98 | } 99 | 100 | private function executeCommand(array $input, array $options = array()) 101 | { 102 | $application = new Application; 103 | $application->add(new ScanCommand); 104 | $tester = new CommandTester($application->find('scan')); 105 | $input['command'] = 'scan'; 106 | $input['path'] = [self::$filename]; 107 | $tester->execute($input, $options); 108 | 109 | return $tester->getDisplay(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/ScannerTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('dispatch') 13 | ->once() 14 | ->with(Scanner::FILE_ISSUE, m::type('\Psecio\Parse\Event\IssueEvent')) 15 | ->mock(); 16 | 17 | $scanner = new Scanner( 18 | $dispatcher, 19 | m::mock('\Psecio\Parse\CallbackVisitor')->shouldReceive('onNodeFailure')->mock() 20 | ); 21 | 22 | $scanner->onNodeFailure( 23 | m::mock('\Psecio\Parse\RuleInterface'), 24 | m::mock('\PhpParser\Node'), 25 | m::mock('\Psecio\Parse\File') 26 | ); 27 | } 28 | 29 | public function testErrorOnPhpsFile() 30 | { 31 | $file = m::mock('\Psecio\Parse\File'); 32 | $file->shouldReceive('isPathMatch')->once()->with('/\.phps$/i')->andReturn(true); 33 | $file->shouldReceive('getContents')->once()->andReturn(''); 34 | 35 | $dispatcher = $this->createErrorDispatcherMock(); 36 | 37 | $scanner = new Scanner( 38 | $dispatcher, 39 | m::mock('\Psecio\Parse\CallbackVisitor')->shouldReceive('onNodeFailure', 'setFile')->mock(), 40 | m::mock('\PhpParser\Parser')->shouldReceive('parse')->andReturn([])->mock(), 41 | m::mock('\PhpParser\NodeTraverser')->shouldReceive('traverse', 'addVisitor')->mock() 42 | ); 43 | 44 | $scanner->scan( 45 | m::mock('\Psecio\Parse\FileIterator') 46 | ->shouldReceive('getIterator') 47 | ->andReturn(new \ArrayIterator([$file])) 48 | ->mock() 49 | ); 50 | } 51 | 52 | public function testErrorOnParseException() 53 | { 54 | $file = m::mock('\Psecio\Parse\File'); 55 | $file->shouldReceive('isPathMatch')->once()->with('/\.phps$/i')->andReturn(true); 56 | $file->shouldReceive('getContents')->once()->andReturn(''); 57 | 58 | $dispatcher = $this->createErrorDispatcherMock(); 59 | 60 | $scanner = new Scanner( 61 | $dispatcher, 62 | m::mock('\Psecio\Parse\CallbackVisitor')->shouldReceive('onNodeFailure', 'setFile')->mock(), 63 | m::mock('\PhpParser\Parser')->shouldReceive('parse')->andThrow(new \PhpParser\Error(''))->mock(), 64 | m::mock('\PhpParser\NodeTraverser') 65 | ->shouldReceive('addVisitor')->shouldReceive('traverse')->mock() 66 | ); 67 | 68 | $scanner->scan( 69 | m::mock('\Psecio\Parse\FileIterator') 70 | ->shouldReceive('getIterator') 71 | ->andReturn(new \ArrayIterator([$file])) 72 | ->mock() 73 | ); 74 | } 75 | 76 | private function createErrorDispatcherMock() 77 | { 78 | $dispatcher = m::mock('\Symfony\Component\EventDispatcher\EventDispatcherInterface'); 79 | $dispatcher->shouldReceive('dispatch')->ordered()->once()->with(Scanner::SCAN_START); 80 | $dispatcher->shouldReceive('dispatch')->ordered()->once()->with( 81 | Scanner::FILE_OPEN, 82 | m::type('\Psecio\Parse\Event\FileEvent') 83 | ); 84 | $dispatcher->shouldReceive('dispatch')->ordered()->once()->with( 85 | Scanner::FILE_ERROR, 86 | m::type('\Psecio\Parse\Event\ErrorEvent') 87 | ); 88 | $dispatcher->shouldReceive('dispatch')->ordered()->once()->with(Scanner::FILE_CLOSE); 89 | $dispatcher->shouldReceive('dispatch')->ordered()->once()->with(Scanner::SCAN_COMPLETE); 90 | 91 | return $dispatcher; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/DocComment/DocCommentTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 34 | ['name' => ['content']], 35 | (new DocComment("/** @name content */"))->getTags() 36 | ); 37 | $this->assertSame( 38 | ['name' => ['content']], 39 | (new DocComment("# @name content"))->getTags() 40 | ); 41 | $this->assertSame( 42 | ['name' => ['content']], 43 | (new DocComment("// @name content"))->getTags() 44 | ); 45 | $this->assertSame( 46 | [ 47 | 'tagOne' => ['content', 'second tag one'], 48 | 'multilineTag' => ['content on multiple lines'], 49 | 'emptyTag' => [''], 50 | 'namespaced\\tag' => ['foobar'] 51 | ], 52 | (new DocComment(self::DOC_BLOCK))->getTags() 53 | ); 54 | } 55 | 56 | public function testSummary() 57 | { 58 | $this->assertSame( 59 | 'summary', 60 | (new DocComment('// summary'))->getSummary() 61 | ); 62 | $this->assertSame( 63 | 'summary', 64 | (new DocComment(' //summary '))->getSummary() 65 | ); 66 | $this->assertSame( 67 | 'summary', 68 | (new DocComment('# summary'))->getSummary() 69 | ); 70 | $this->assertSame( 71 | 'summary', 72 | (new DocComment('/* summary */'))->getSummary() 73 | ); 74 | $this->assertSame( 75 | 'summary', 76 | (new DocComment('/** summary */'))->getSummary() 77 | ); 78 | $this->assertSame( 79 | 'Multiline summary', 80 | (new DocComment(self::DOC_BLOCK))->getSummary() 81 | ); 82 | } 83 | 84 | public function testBody() 85 | { 86 | $this->assertSame( 87 | "Body\n\nSpanning multiple\nparagrafs", 88 | (new DocComment(self::DOC_BLOCK))->getBody() 89 | ); 90 | } 91 | 92 | public function testGetMatchingTags() 93 | { 94 | $block = <<assertEquals($expectedTag1, $dc->getMatchingTags('tag1')); 111 | $this->assertEquals($expectedTag2, $dc->getMatchingTags('tag2')); 112 | $this->assertEquals([], $dc->getMatchingTags('notATag')); 113 | } 114 | 115 | public function testGetIMatchingTags() 116 | { 117 | $block = <<assertEquals($expectedTag1, $dc->getIMatchingTags('tag1')); 130 | $this->assertEquals([], $dc->getIMatchingTags('notATag')); 131 | } 132 | 133 | public function testRawComment() 134 | { 135 | $doc = new DocComment(self::DOC_BLOCK); 136 | $this->assertSame(self::DOC_BLOCK, $doc->getRawComment()); 137 | } 138 | } -------------------------------------------------------------------------------- /tests/Rule/HardcodedSensitiveValuesTest.php: -------------------------------------------------------------------------------- 1 | {$key} = "value";', true], 79 | ['const USER = "username";', false], 80 | ['define("user", "username");', false], 81 | ['$this->events[$event] = array();', true], 82 | ['define($const_name, $const_value);', true], 83 | ]; 84 | } 85 | 86 | protected function buildTest() 87 | { 88 | return new HardcodedSensitiveValues(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/CallbackVisitor.php: -------------------------------------------------------------------------------- 1 | bool 25 | */ 26 | private $enabledRules; 27 | 28 | /** 29 | * @var callable Fail callback 30 | */ 31 | private $callback; 32 | 33 | /** 34 | * @var File Current file under evaluation 35 | */ 36 | private $file; 37 | 38 | /** 39 | * @var bool If false, ignore all annotations 40 | */ 41 | private $useAnnotations; 42 | 43 | /** 44 | * @var DocCommentFactoryInterface 45 | */ 46 | private $docCommentFactory; 47 | 48 | /** 49 | * Inject rule collection 50 | * 51 | * @param RuleCollection $ruleCollection 52 | * @param bool $useAnnotations If false, ignore all annotations 53 | */ 54 | public function __construct(RuleCollection $ruleCollection, 55 | DocCommentFactoryInterface $docCommentFactory, 56 | $useAnnotations) 57 | { 58 | $this->ruleCollection = $ruleCollection; 59 | $this->enabledRules = []; 60 | 61 | foreach ($this->ruleCollection as $rule) { 62 | $this->enabledRules[strtolower($rule->getName())] = true; 63 | } 64 | 65 | $this->useAnnotations = $useAnnotations; 66 | $this->docCommentFactory = $docCommentFactory; 67 | } 68 | 69 | /** 70 | * Register failure callback 71 | * 72 | * @param callable $callback 73 | * @return void 74 | */ 75 | public function onNodeFailure(callable $callback) 76 | { 77 | $this->callback = $callback; 78 | } 79 | 80 | /** 81 | * Set file under evaluation 82 | * 83 | * @param File $file 84 | * @return void 85 | */ 86 | public function setFile(File $file) 87 | { 88 | $this->file = $file; 89 | } 90 | 91 | /** 92 | * Interface function called when node is first hit 93 | * 94 | * @param Node $node 95 | */ 96 | public function enterNode(Node $node) 97 | { 98 | if ($this->useAnnotations) { 99 | $this->updateRuleFilters($node); 100 | } 101 | 102 | foreach ($this->ruleCollection as $rule) { 103 | $this->evaluateRule($node, $rule); 104 | } 105 | } 106 | 107 | protected function evaluateRule($node, $rule) 108 | { 109 | if (!$this->enabledRules[strtolower($rule->getName())]) { 110 | return; 111 | } 112 | 113 | if (!$rule->isValid($node)) { 114 | call_user_func($this->callback, $rule, $node, $this->file); 115 | } 116 | } 117 | 118 | public function leaveNode(Node $node) 119 | { 120 | if (!$node->hasAttribute('oldEnabledRules')) { 121 | return; 122 | } 123 | 124 | // Restore rules as they were before this node 125 | $this->enabledRules = $node->getAttribute('oldEnabledRules'); 126 | } 127 | 128 | private function updateRuleFilters($node) 129 | { 130 | $docBlock = $node->getDocComment(); 131 | if (empty($docBlock)) { 132 | return; 133 | } 134 | 135 | $node->setAttribute('oldEnabledRules', $this->enabledRules); 136 | $this->enabledRules = $this->evaluateDocBlock($docBlock, $this->enabledRules); 137 | } 138 | 139 | private function evaluateDocBlock($docBlock, $rules) 140 | { 141 | $comment = $this->docCommentFactory->createDocComment($docBlock); 142 | 143 | $this->checkTags($comment, $rules, self::ENABLE_TAG, true); 144 | $this->checkTags($comment, $rules, self::DISABLE_TAG, false); 145 | 146 | return $rules; 147 | } 148 | 149 | private function checkTags(DocComment\DocCommentInterface $comment, &$rules, $tag, $value) 150 | { 151 | $tags = $comment->getIMatchingTags($tag); 152 | 153 | foreach ($tags as $rule) { 154 | // Get the first word from content. This allows you to add comments to rules. 155 | $ruleName = trim(strtolower(strtok($rule, '//'))); 156 | $rules[$ruleName] = $value; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tests/FileIteratorTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException('RuntimeException'); 50 | new FileIterator([]); 51 | } 52 | 53 | /** 54 | * Test that the directory and files were created correctly and 55 | * the iterator contains them 56 | */ 57 | public function testDirectoryPath() 58 | { 59 | $iterator = new FileIterator([self::$testDir]); 60 | $paths = $iterator->getPaths(); 61 | 62 | $this->assertCount(0, array_diff($paths, self::$expectedFiles)); 63 | } 64 | 65 | public function testFilenamePath() 66 | { 67 | $this->assertArrayHasKey( 68 | __FILE__, 69 | iterator_to_array(new FileIterator([__FILE__])), 70 | __FILE__ . ' should be found in iterator' 71 | ); 72 | } 73 | 74 | public function testIgnoreFilename() 75 | { 76 | $this->assertArrayHasKey( 77 | __FILE__, 78 | iterator_to_array(new FileIterator([__FILE__], [__FILE__])), 79 | __FILE__ . ' should not be ignored as ignore filters should not matter when files are added' 80 | ); 81 | $this->assertArrayNotHasKey( 82 | __FILE__, 83 | iterator_to_array(new FileIterator([__DIR__], [__FILE__])), 84 | __FILE__ . ' should be ignored when ' . __DIR__ . ' is ignored' 85 | ); 86 | } 87 | 88 | public function testIgnoreDirectory() 89 | { 90 | $this->assertEmpty( 91 | iterator_to_array(new FileIterator([__DIR__], [__DIR__])), 92 | 'All files in dir should be ignored when ' . __DIR__ . ' is ignored' 93 | ); 94 | } 95 | 96 | /** 97 | * @group single 98 | */ 99 | public function testIgnoreNonCompletePaths() 100 | { 101 | $expected = realpath(self::expand('dir2/file.php')); 102 | $iterator = new FileIterator(self::$expectedFiles, [self::expand('dir')]); 103 | 104 | $this->assertArrayHasKey( 105 | $expected, 106 | $iterator->toArray(), 107 | "'$expected' should not be ignored as 'dir' should not match 'dir2'" 108 | ); 109 | } 110 | 111 | public function testInvalidIgnorePath() 112 | { 113 | $this->assertArrayHasKey( 114 | __FILE__, 115 | iterator_to_array(new FileIterator([__FILE__], ['this/really/does/not/exist'])), 116 | 'Adding a non existing path to the ignore list should not affect anything' 117 | ); 118 | } 119 | 120 | public function testFileExtensions() 121 | { 122 | $this->assertArrayNotHasKey( 123 | __FILE__, 124 | iterator_to_array(new FileIterator([__DIR__], [], ['txt'])), 125 | __FILE__ . ' should be ignored as it does not have a .txt extension' 126 | ); 127 | $this->assertArrayHasKey( 128 | __FILE__, 129 | iterator_to_array(new FileIterator([__FILE__], [], ['txt'])), 130 | __FILE__ . ' should be included as extensions should not matter when a file is added' 131 | ); 132 | } 133 | 134 | public function testCountable() 135 | { 136 | $this->assertSame( 137 | count(self::$expectedFiles), 138 | count(new FileIterator([self::$testDir])), 139 | self::$testDir . 'should contain the correct number of test files' 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Subscriber/ConsoleReport.php: -------------------------------------------------------------------------------- 1 | fileCount = 0; 39 | $this->issues = []; 40 | $this->errors = []; 41 | } 42 | 43 | /** 44 | * Write report on scan complete 45 | * 46 | * @return void 47 | */ 48 | public function onScanComplete() 49 | { 50 | $this->output->writeln($this->getReport()); 51 | } 52 | 53 | /** 54 | * Increment files scanned counter 55 | * 56 | * @param FileEvent $event 57 | * @return void 58 | */ 59 | public function onFileOpen(FileEvent $event) 60 | { 61 | $this->fileCount++; 62 | } 63 | 64 | /** 65 | * Save issue event 66 | * 67 | * @param IssueEvent $event 68 | * @return void 69 | */ 70 | public function onFileIssue(IssueEvent $event) 71 | { 72 | $this->issues[] = $event; 73 | } 74 | 75 | /** 76 | * Save error event 77 | * 78 | * @param ErrorEvent $event 79 | * @return void 80 | */ 81 | public function onFileError(ErrorEvent $event) 82 | { 83 | $this->errors[] = $event; 84 | } 85 | 86 | /** 87 | * Format using different formats pending on $number 88 | * 89 | * @param string $singular Format used in singularis 90 | * @param string $plural Format used in pluralis 91 | * @param int|float $count Format argument and numerus marker 92 | * @return string 93 | */ 94 | private function pluralize($singular, $plural, $count) 95 | { 96 | return $count == 1 ? sprintf($singular, $count) : sprintf($plural, $count); 97 | } 98 | 99 | /** 100 | * Get report 101 | * 102 | * @return string 103 | */ 104 | private function getReport() 105 | { 106 | return "\n\n" . ($this->errors || $this->issues ? $this->getFailureReport() : $this->getPassReport()); 107 | } 108 | 109 | /** 110 | * Get info on scanned files 111 | * 112 | * @return string 113 | */ 114 | private function getPassReport() 115 | { 116 | return $this->pluralize( 117 | "OK (%d file scanned)", 118 | "OK (%d files scanned)", 119 | $this->fileCount 120 | ); 121 | } 122 | 123 | /** 124 | * Get info on errors and issues 125 | * 126 | * @return string 127 | */ 128 | private function getFailureReport() 129 | { 130 | return $this->getErrorReport() 131 | . $this->getIssueReport() 132 | . sprintf( 133 | "FAILURES!\nScanned: %d, Errors: %d, Issues: %d.", 134 | $this->fileCount, 135 | count($this->errors), 136 | count($this->issues) 137 | ); 138 | } 139 | 140 | /** 141 | * Get issue report 142 | * 143 | * @return string 144 | */ 145 | private function getIssueReport() 146 | { 147 | $str = ''; 148 | 149 | if ($this->issues) { 150 | $str .= $this->pluralize( 151 | "There was %d issue\n\n", 152 | "There were %d issues\n\n", 153 | count($this->issues) 154 | ); 155 | } 156 | 157 | foreach ($this->issues as $index => $issueEvent) { 158 | $str .= sprintf( 159 | "%d) %s on line %d\n%s\n> %s\n", 160 | $index + 1, 161 | $issueEvent->getFile()->getPath(), 162 | $issueEvent->getNode()->getLine(), 163 | $issueEvent->getRule()->getDescription(), 164 | implode("\n> ", $issueEvent->getFile()->fetchNode($issueEvent->getNode())) 165 | ); 166 | $str .= "For more information execute 'psecio-parse rules {$issueEvent->getRule()->getName()}'\n\n"; 167 | } 168 | 169 | return $str; 170 | } 171 | 172 | /** 173 | * Get error report 174 | * 175 | * @return string 176 | */ 177 | private function getErrorReport() 178 | { 179 | $str = ''; 180 | 181 | if ($this->errors) { 182 | $str .= $this->pluralize( 183 | "There was %d error\n\n", 184 | "There were %d errors\n\n", 185 | count($this->errors) 186 | ); 187 | } 188 | 189 | foreach ($this->errors as $index => $errorEvent) { 190 | $str .= sprintf( 191 | "%d) %s\n%s\n\n", 192 | $index + 1, 193 | $errorEvent->getFile()->getPath(), 194 | $errorEvent->getMessage() 195 | ); 196 | } 197 | 198 | return $str; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /tests/Rule/RuleTestCase.php: -------------------------------------------------------------------------------- 1 | parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); 27 | } 28 | 29 | /** 30 | * Method to create the test to evaluate 31 | * 32 | * This should return an instantiated object of the class that is being evaluated 33 | * 34 | * @return RuleInterface An object of the type being tested 35 | */ 36 | abstract protected function buildTest(); 37 | 38 | /** 39 | * PHPUnit provider to provide samples and results 40 | * 41 | * This method provides a list of samples and expected results that this unit test 42 | * should test against. The structure should be: 43 | * 44 | * [ ['valid php code', ], 45 | * [ ... ], 46 | * ... 47 | * ] 48 | * 49 | * Note that the actual test method prefixes the sample code with 'parseSampleProvider() as $index => $args) { 65 | list($code, $result) = $args; 66 | $this->assertParseTest( 67 | $result, 68 | $code, 69 | sprintf('Sample #%d from %s::parseSampleProvider failed.', $index, get_class($this)) 70 | ); 71 | } 72 | } 73 | 74 | public function testDescription() 75 | { 76 | $this->assertInternalType( 77 | 'string', 78 | $this->buildTest()->getDescription(), 79 | 'getDescription() must return a string' 80 | ); 81 | } 82 | 83 | public function testLongDescription() 84 | { 85 | $this->assertInternalType( 86 | 'string', 87 | $this->buildTest()->getLongDescription(), 88 | 'getLongDescription() must return a string' 89 | ); 90 | } 91 | 92 | /** 93 | * Assert that running $test against $code results in $expected 94 | * 95 | * Note taht $message does not replace the message from the assertion, 96 | * only augments it. 97 | * 98 | * @param string $code The PHP code to parse and evaulate 99 | * @param mixed $expected The expected result of the $test 100 | * @param string $message Message to be displayed on failure 101 | */ 102 | public function assertParseTest($expected, $code, $message = '') 103 | { 104 | $message = sprintf( 105 | "%sThe parser scan should have %s the test.\nTested code was:\n%s", 106 | empty($message) ? '' : ($message . "\n"), 107 | $expected ? 'passed' : 'failed', 108 | $this->formatCodeForMessage($code) 109 | ); 110 | 111 | $actual = $this->scan($code); 112 | 113 | $this->assertSame($expected, $actual, $message); 114 | } 115 | 116 | /** 117 | * Assert that parsing $code with buildTest()'s Test returns false 118 | * 119 | * @param string $code Code to test 120 | * @param string $message Message to display on failure 121 | */ 122 | public function assertParseTestFalse($code, $message = '') 123 | { 124 | $this->assertParseTest(false, $code, $message); 125 | } 126 | 127 | /** 128 | * Assert that parsing $code with buildTest()'s Test returns true 129 | * 130 | * @param string $code Code to test 131 | * @param string $message Message to display on failure 132 | */ 133 | public function assertParseTestTrue($code, $message = '') 134 | { 135 | $this->assertParseTest(true, $code, $message); 136 | } 137 | 138 | /** 139 | * Format a code string so it displays nicely in an assertion message 140 | * 141 | * Prefixes each line of the code with " > ". 142 | * 143 | * @param string $code The code to format 144 | * 145 | * @return string The formatted code 146 | */ 147 | protected function formatCodeForMessage($code) 148 | { 149 | $linePrefix = " > "; 150 | return $linePrefix . str_replace("\n", "\n" . $linePrefix, trim($code)); 151 | } 152 | 153 | /** 154 | * Scan PHP code and return the result 155 | * 156 | * @param string $code The code to scan 157 | * 158 | * @return bool The results visiting all the nodes from the parsed $code 159 | */ 160 | protected function scan($code) 161 | { 162 | $visitor = new RuleTestVisitor($this->buildTest()); 163 | $traverser = new NodeTraverser; 164 | $traverser->addVisitor($visitor); 165 | $traverser->traverse($this->parser->parse('result; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/DocComment/DocComment.php: -------------------------------------------------------------------------------- 1 | self::STATE_SUMMARY, 45 | self::STATE_SUMMARY => self::STATE_BODY, 46 | self::STATE_BODY => self::STATE_BODY, 47 | self::STATE_TAG => self::STATE_TAG, 48 | self::STATE_IN_TAG => self::STATE_IGNORE, 49 | self::STATE_IGNORE => self::STATE_IGNORE 50 | ]; 51 | 52 | /** 53 | * @var integer Current parsing state 54 | */ 55 | private $state = self::STATE_INIT; 56 | 57 | /** 58 | * @var string Name of tag being parsed 59 | */ 60 | private $currentTag; 61 | 62 | /** 63 | * @var string Content of tag being parsed 64 | */ 65 | private $currentTagContent; 66 | 67 | /** 68 | * @var string Comment summary 69 | */ 70 | private $summary = ''; 71 | 72 | /** 73 | * @var string Comment body 74 | */ 75 | private $body = ''; 76 | 77 | /** 78 | * @var string The original, raw comment 79 | */ 80 | private $rawComment = ''; 81 | 82 | /** 83 | * @var array Tags in comment 84 | */ 85 | private $tags = []; 86 | 87 | /** 88 | * Parse comment 89 | * 90 | * @param string $comment 91 | */ 92 | public function __construct($comment) 93 | { 94 | $this->rawComment = $comment; 95 | foreach (preg_split("/\r\n|\n|\r/", $comment) as $line) { 96 | $line = ltrim($line, "\t\0\x0B /*#"); 97 | $line = rtrim($line, "\t\0\x0B /*"); 98 | $this->parseLine($line); 99 | } 100 | $this->saveCurrentTag(); 101 | } 102 | 103 | /** 104 | * Get the original, raw comment 105 | * 106 | * @return string 107 | */ 108 | public function getRawComment() 109 | { 110 | return $this->rawComment; 111 | } 112 | 113 | /** 114 | * Get doc comment summary 115 | * 116 | * @return string 117 | */ 118 | public function getSummary() 119 | { 120 | return trim($this->summary); 121 | } 122 | 123 | /** 124 | * Get doc block body 125 | * 126 | * @return string 127 | */ 128 | public function getBody() 129 | { 130 | return trim($this->body); 131 | } 132 | 133 | /** 134 | * Get defined tags 135 | * 136 | * @return array 137 | */ 138 | public function getTags() 139 | { 140 | return $this->tags; 141 | } 142 | 143 | /** 144 | * Get tag values matching $tagName 145 | * 146 | * @param string $tagName 147 | * 148 | * @return array List of matching values 149 | */ 150 | public function getMatchingTags($tagName) 151 | { 152 | if (isset($this->tags[$tagName])) { 153 | return $this->tags[$tagName]; 154 | } 155 | 156 | return array(); 157 | } 158 | 159 | /** 160 | * Get tag values matching $tagName, case insensitively 161 | * 162 | * @param string $tagName 163 | * 164 | * @return array List of matching values 165 | */ 166 | public function getIMatchingTags($tagName) 167 | { 168 | $tagName = strtolower($tagName); 169 | 170 | $result = array(); 171 | 172 | foreach ($this->tags as $name => $values) { 173 | if (strtolower($name) == $tagName) { 174 | $result = array_merge($result, $values); 175 | } 176 | } 177 | 178 | return $result; 179 | } 180 | 181 | /** 182 | * Sateful line parser 183 | * 184 | * @param string $line 185 | * @return void 186 | */ 187 | private function parseLine($line) 188 | { 189 | if (empty($line)) { 190 | $this->state = $this->emptyLineStateTransitions[$this->state]; 191 | } 192 | 193 | if (strpos($line, '@') === 0) { 194 | $this->state = self::STATE_TAG; 195 | } 196 | 197 | switch ($this->state) { 198 | case self::STATE_INIT: 199 | case self::STATE_SUMMARY: 200 | $this->summary .= ' ' . $line; 201 | break; 202 | case self::STATE_BODY: 203 | $this->body .= "\n" . $line; 204 | break; 205 | case self::STATE_TAG: 206 | $this->saveCurrentTag(); 207 | if (preg_match('/^@([\w\\\\]+)\s*(.*?)\s*$/', $line, $matches)) { 208 | list(, $this->currentTag, $this->currentTagContent) = $matches; 209 | } 210 | $this->state = self::STATE_IN_TAG; 211 | break; 212 | case self::STATE_IN_TAG: 213 | $this->currentTagContent .= ' ' . $line; 214 | break; 215 | } 216 | } 217 | 218 | /** 219 | * Save the tag currently being parsed 220 | * 221 | * @return void 222 | */ 223 | private function saveCurrentTag() 224 | { 225 | if (isset($this->currentTag)) { 226 | if (!isset($this->tags[$this->currentTag])) { 227 | $this->tags[$this->currentTag] = []; 228 | } 229 | $this->tags[$this->currentTag][] = trim($this->currentTagContent); 230 | unset($this->currentTag); 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/FileIterator.php: -------------------------------------------------------------------------------- 1 | [], 'files' => []]; 28 | 29 | /** 30 | * @var string[] List of file extensions to include when scanning dirs 31 | */ 32 | private $extensions = []; 33 | 34 | /** 35 | * Append paths to iterator 36 | * 37 | * @param string[] $paths List of paths to scan 38 | * @param string[] $ignorePaths List of paths to ignore 39 | * @param string[] $extensions List of file extensions to include when scanning dirs 40 | * @throws RuntimeException If the list of paths to scan is empty 41 | */ 42 | public function __construct(array $paths, array $ignorePaths = array(), array $extensions = array('php')) 43 | { 44 | if (empty($paths)) { 45 | throw new RuntimeException('No paths to scan'); 46 | } 47 | 48 | $this->addExtensions($extensions); 49 | 50 | foreach ($ignorePaths as $path) { 51 | $this->addIgnorePath($path); 52 | } 53 | 54 | foreach ($paths as $path) { 55 | $this->addPath($path); 56 | } 57 | } 58 | 59 | /** 60 | * Add list of file extensions to include when scanning dirs 61 | * 62 | * @param string[] $extensions 63 | * @return void 64 | */ 65 | public function addExtensions(array $extensions) 66 | { 67 | $this->extensions = array_merge($this->extensions, $extensions); 68 | } 69 | 70 | /** 71 | * Add a path to the list of ignored paths 72 | * 73 | * Non existing paths are silently skipped. 74 | * 75 | * @param string $path 76 | * @return void 77 | */ 78 | public function addIgnorePath($path) 79 | { 80 | $realPath = realpath($path); 81 | if ($realPath === false) { 82 | return false; 83 | } 84 | $splFileInfo = new SplFileInfo($realPath); 85 | 86 | if ($splFileInfo->isFile()) { 87 | $this->ignorePaths['files'][] = $splFileInfo->getRealPath(); 88 | } elseif ($splFileInfo->isDir()) { 89 | $this->ignorePaths['dirs'][] = $splFileInfo->getRealPath() . DIRECTORY_SEPARATOR; 90 | } 91 | } 92 | 93 | /** 94 | * Add a path to iterator 95 | * 96 | * @param string $path 97 | * @return void 98 | */ 99 | public function addPath($path) 100 | { 101 | if (is_dir($path)) { 102 | $this->addDirectory($path); 103 | } else { 104 | $realPath = realpath($path); 105 | if ($realPath === false) { 106 | return false; 107 | } 108 | $this->addFile(new SplFileInfo($realPath)); 109 | } 110 | } 111 | 112 | /** 113 | * Get iterator whith file paths as keys and File objects as values 114 | * 115 | * @return ArrayIterator 116 | */ 117 | public function getIterator() 118 | { 119 | return new ArrayIterator($this->files); 120 | } 121 | 122 | /** 123 | * Return a count of files in iterator 124 | * 125 | * @return integer 126 | */ 127 | public function count() 128 | { 129 | return count($this->files); 130 | } 131 | 132 | /** 133 | * Recursicely add files in directory 134 | * 135 | * @see FileIterator::isValidFile() Only files that are considered valid are added 136 | * 137 | * @param string $directory Pathname of directory 138 | * @return void 139 | */ 140 | private function addDirectory($directory) 141 | { 142 | $directory = realpath($directory); 143 | $iterator = new RecursiveIteratorIterator( 144 | new RecursiveDirectoryIterator( 145 | $directory, 146 | FilesystemIterator::SKIP_DOTS 147 | ) 148 | ); 149 | foreach ($iterator as $splFileInfo) { 150 | if ($this->isValidFile($splFileInfo)) { 151 | $this->addFile($splFileInfo); 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * Add a SplFileInfo object to iterator 158 | * 159 | * @param SplFileInfo $splFileInfo 160 | * @return void 161 | */ 162 | private function addFile(SplFileInfo $splFileInfo) 163 | { 164 | $this->files[$splFileInfo->getRealPath()] = new File($splFileInfo); 165 | } 166 | 167 | /** 168 | * Check of file should be included 169 | * 170 | * Returns false if the file extension is not valid or 171 | * if the file is in the ignore list. 172 | * 173 | * @param SplFileInfo $splFileInfo 174 | * @return boolean 175 | */ 176 | private function isValidFile(SplFileInfo $splFileInfo) 177 | { 178 | if (!in_array($splFileInfo->getExtension(), $this->extensions, true)) { 179 | return false; 180 | } 181 | 182 | $realPath = $splFileInfo->getRealPath(); 183 | 184 | if (in_array($realPath, $this->ignorePaths['files'], true)) { 185 | return false; 186 | } 187 | 188 | foreach ($this->ignorePaths['dirs'] as $ignoreDir) { 189 | if (strpos($realPath, $ignoreDir) === 0) { 190 | return false; 191 | } 192 | } 193 | 194 | return true; 195 | } 196 | 197 | /** 198 | * Convert the interator to an array (return current files) 199 | * 200 | * @return array Set of current files 201 | */ 202 | public function toArray() 203 | { 204 | return $this->files; 205 | } 206 | 207 | /** 208 | * Get the full paths for the current files 209 | * 210 | * @return array Set of file paths 211 | */ 212 | public function getPaths() 213 | { 214 | $paths = []; 215 | foreach ($this->files as $file) { 216 | $paths[] = $file->getPath(); 217 | } 218 | return $paths; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Parse: A PHP Security Scanner 2 | ============================= 3 | 4 | [![Packagist Version](https://img.shields.io/packagist/v/psecio/parse.svg?style=flat-square)](https://packagist.org/packages/psecio/parse) 5 | [![Build Status](https://img.shields.io/travis/psecio/parse/master.svg?style=flat-square)](https://travis-ci.org/psecio/parse) 6 | 7 | > **PLEASE NOTE:** This tool is still in a very early stage. The work continues... 8 | 9 | The *Parse* scanner is a static scanning tool to review your PHP code for potential security-related 10 | issues. A static scanner means that the code is not executed and tested via a web interface (that's 11 | dynamic testing). Instead, the scanner looks through your code and checks for certain markers and notifies 12 | you when any are found. 13 | 14 | For example, you really shouldn't be using [eval](http://php.net/eval) in your code anywhere if you can 15 | help it. When the scanner runs, it will parse down each of your files and look for any `eval()` calls. 16 | If it finds any, it adds that match to the file and reports it in the results. 17 | 18 | 19 | Installation 20 | ------------ 21 | Install as a development dependency in your project using [composer](https://getcomposer.org/): 22 | 23 | composer require --dev psecio/parse 24 | 25 | The path to the installed executable may vary depending on your 26 | [bin-dir](https://getcomposer.org/doc/04-schema.md#config) setting. With the 27 | default value parse is located at `vendor/bin/psecio-parse`. 28 | 29 | For a system-wide installation use: 30 | 31 | composer global require psecio/parse 32 | 33 | Make sure you have `~/.composer/vendor/bin/` in your path. 34 | 35 | 36 | Usage 37 | ----- 38 | > **NOTE:** In version **0.6** the executable was renamed **psecio-parse**. In earlier 39 | > versions the tool was simply named **parse**. 40 | 41 | > **NOTE:** In version **0.4** and earlier the `--target` option was used to specify the 42 | > project path, this is no longer supported. Use the syntax below. 43 | 44 | To use the scanner execute it from the command line: 45 | 46 | psecio-parse scan /path/to/my/project 47 | 48 | For more detailed information see the `help` and `list` commands. 49 | 50 | psecio-parse help scan 51 | 52 | ### Output formats 53 | 54 | Currently console (dots), xml and json output formats are available. Set format with the `--format` option. 55 | 56 | psecio-parse scan --format=xml /path/to/my/project 57 | psecio-parse scan --format=dots /path/to/my/project 58 | psecio-parse scan --format=json /path/to/my/project 59 | 60 | The console formats supports setting the verbosity using the `-v` or `-vv` switch. 61 | 62 | psecio-parse scan -vv /path/to/my/project 63 | 64 | If your platform does not support ANSI codes, or if you want to redirect the console output 65 | to a file, use the `--no-ansi` option. 66 | 67 | psecio-parse scan --no-ansi /path/to/my/project > filename 68 | 69 | ### Listing the checks 70 | 71 | You can also get a listing of the current checks being done with the `rules` command: 72 | 73 | psecio-parse rules 74 | 75 | ### Managing rules to run 76 | 77 | There are several ways to control which rules are run. You can specifically include rules using 78 | the `--include-rules` option, specifically exclude them with `--exclude-rules`, turn them on and 79 | off on a case-by-case basis using annotations, and disable annotations using 80 | `--disable-annotations`. 81 | 82 | #### Excluding and Including rules 83 | 84 | By default, `psecio-parse scan` includes all available rules in its scan. By using 85 | `--exclude-rules` and `--include-rules`, the rules included can be reduced. 86 | 87 | Any rules specified by `--exclude-rules` are explicitly excluded from the scan, regardless of any 88 | other options selected. These rules cannot be added back to the scan, short of re-running the scan 89 | with different options. Invalid rules are silently ignored. 90 | 91 | If `--include-rules` is provided, only those rules specified can be used. No other rules are 92 | checked. Note that rules that aren't available (whether they do not exist or `--excluded-rules` is 93 | used to exclude them) cannot be included. Invalid rules are silently ignored. 94 | 95 | #### Annotations 96 | 97 | Rules can be enabled and disabled using DocBlock annotations. These are comments in the code being 98 | scanned that tells *Parse* to specifically enable or disable a rule for the block of code the 99 | DocBlock applies to. 100 | 101 | * `@psecio\parse\disable `: Tells *Parse* to ignore the given rule for the scope of the 102 | DocBlock. 103 | * `@psecio\parse\enable `: Tells *Parse* to enable the given rule for the scope of the 104 | DocBlock. This can be used to re-enable a particular rule when `@psecio\parse\disable` has been 105 | applied to the containing scope. 106 | 107 | Note that annotations cannot enable tests that have been omitted via the command line options. If 108 | a test is disabled at the command line, it is disabled for the entire scan, regardless of any 109 | annotations. 110 | 111 | Comments can be added after `` following a dobule-slash (`//`) comment separator. It is 112 | recommended that comments be used to indicate why the rule has been disabled or enabled. 113 | 114 | To disable the use of annotations, use the `--disable-annotations` option. 115 | 116 | See the `examples` directory for some examples of the use of annotations for *Parse*. 117 | 118 | The Checks 119 | ---------- 120 | Here's the current list of checks: 121 | 122 | - Warn when sensitive values are committed (as defined by a variable like "username" set to a string) 123 | - Warn when `display_errors` is enabled manually 124 | - Avoid the use of `eval()` 125 | - Avoid the use of `exit` or `die()` 126 | - Avoid the use of logical operators (ex. using `and` over `&&`) 127 | - Avoid the use of the `ereg*` functions (now deprecated) 128 | - Ensure that the second parameter of `extract` is set to not overwrite (*not* EXTR_OVERWRITE) 129 | - Checking output methods (`echo`, `print`, `printf`, `print_r`, `vprintf`, `sprintf`) that use variables in their options 130 | - Ensuring you're not using `echo` with `file_get_contents` 131 | - Testing for the system execution functions and shell exec (backticks) 132 | - Use of `readfile`, `readlink` and `readgzfile` 133 | - Using `parse_str` or `mb_parse_str` (writes values to the local scope) 134 | - Warn if a `.phps` file is found 135 | - Using `session_regenerate_id` either without a parameter or using false 136 | - Avoid use of `$_REQUEST` (know where your data is coming from) 137 | - Don't use `mysql_real_escape_string` 138 | - Avoiding use of `import_request_variables` 139 | - Avoid use of `$GLOBALS` 140 | - Ensure the use of type checking validating against booleans (`===`) 141 | - Ensure that the `/e` modifier isn't used in regular expressions (execute) 142 | - Using concatenation in `header()` calls 143 | - Avoiding the use of $http_raw_post_data 144 | 145 | Plenty more to come... (yup, `@todo`) 146 | 147 | 148 | TODO 149 | ---- 150 | See the current issues list for `@todo` items... 151 | 152 | Parse is covered under the MIT license. 153 | 154 | @author Chris Cornutt (ccornutt@phpdeveloper.org) 155 | -------------------------------------------------------------------------------- /src/Command/ScanCommand.php: -------------------------------------------------------------------------------- 1 | setName('scan') 41 | ->setDescription('Scans paths for possible security issues') 42 | ->addArgument( 43 | 'path', 44 | InputArgument::OPTIONAL|InputArgument::IS_ARRAY, 45 | 'Paths to scan', 46 | [] 47 | ) 48 | ->addOption( 49 | 'format', 50 | 'f', 51 | InputOption::VALUE_REQUIRED, 52 | 'Output format (progress, dots, xml or json)', 53 | 'progress' 54 | ) 55 | ->addOption( 56 | 'ignore-paths', 57 | 'i', 58 | InputOption::VALUE_REQUIRED, 59 | 'Comma-separated list of paths to ignore', 60 | '' 61 | ) 62 | ->addOption( 63 | 'extensions', 64 | 'x', 65 | InputOption::VALUE_REQUIRED, 66 | 'Comma-separated list of file extensions to parse', 67 | 'php,phps,phtml,php5' 68 | ) 69 | ->addOption( 70 | 'whitelist-rules', 71 | 'w', 72 | InputOption::VALUE_REQUIRED, 73 | 'Comma-separated list of rules to use', 74 | '' 75 | ) 76 | ->addOption( 77 | 'blacklist-rules', 78 | 'b', 79 | InputOption::VALUE_REQUIRED, 80 | 'Comma-separated list of rules to skip', 81 | '' 82 | ) 83 | ->addOption( 84 | 'disable-annotations', 85 | 'd', 86 | InputOption::VALUE_NONE, 87 | 'Skip all annotation-based rule toggles.' 88 | ) 89 | ->setHelp( 90 | "Scan paths for possible security issues:\n\n psecio-parse %command.name% /path/to/src\n" 91 | ); 92 | } 93 | 94 | /** 95 | * Execute the "scan" command 96 | * 97 | * @param InputInterface $input Input object 98 | * @param OutputInterface $output Output object 99 | * @throws RuntimeException If output format is not valid 100 | * @return void 101 | */ 102 | protected function execute(InputInterface $input, OutputInterface $output) 103 | { 104 | $dispatcher = new EventDispatcher; 105 | 106 | $exitCode = new ExitCodeCatcher; 107 | $dispatcher->addSubscriber($exitCode); 108 | 109 | $fileIterator = new FileIterator( 110 | $input->getArgument('path'), 111 | $this->parseCsv($input->getOption('ignore-paths')), 112 | $this->parseCsv($input->getOption('extensions')) 113 | ); 114 | 115 | $format = strtolower($input->getOption('format')); 116 | switch ($format) { 117 | case 'dots': 118 | case 'progress': 119 | $output->writeln("Parse: A PHP Security Scanner\n"); 120 | if ($output->isVeryVerbose()) { 121 | $dispatcher->addSubscriber( 122 | new ConsoleDebug($output) 123 | ); 124 | } elseif ($output->isVerbose()) { 125 | $dispatcher->addSubscriber( 126 | new ConsoleLines($output) 127 | ); 128 | } elseif ('progress' == $format && $output->isDecorated()) { 129 | $dispatcher->addSubscriber( 130 | new ConsoleProgressBar(new ProgressBar($output, count($fileIterator))) 131 | ); 132 | } else { 133 | $dispatcher->addSubscriber( 134 | new ConsoleDots($output) 135 | ); 136 | } 137 | $dispatcher->addSubscriber(new ConsoleReport($output)); 138 | break; 139 | case 'xml': 140 | $dispatcher->addSubscriber(new Xml($output)); 141 | break; 142 | case 'json': 143 | $dispatcher->addSubscriber(new Json($output)); 144 | break; 145 | default: 146 | throw new RuntimeException("Unknown output format '{$input->getOption('format')}'"); 147 | } 148 | 149 | $ruleFactory = new RuleFactory( 150 | $this->parseCsv($input->getOption('whitelist-rules')), 151 | $this->parseCsv($input->getOption('blacklist-rules')) 152 | ); 153 | 154 | $ruleCollection = $ruleFactory->createRuleCollection(); 155 | 156 | $ruleNames = implode(',', array_map( 157 | function (RuleInterface $rule) { 158 | return $rule->getName(); 159 | }, 160 | $ruleCollection->toArray() 161 | )); 162 | 163 | $dispatcher->dispatch(Events::DEBUG, new MessageEvent("Using ruleset $ruleNames")); 164 | 165 | $docCommentFactory = new DocCommentFactory(); 166 | 167 | $scanner = new Scanner( 168 | $dispatcher, 169 | new CallbackVisitor( 170 | $ruleCollection, 171 | $docCommentFactory, 172 | !$input->getOption('disable-annotations') 173 | ) 174 | ); 175 | $scanner->scan($fileIterator); 176 | 177 | return $exitCode->getExitCode(); 178 | } 179 | 180 | /** 181 | * Parse comma-separated values from string 182 | * 183 | * Using array_filter ensures that an empty array is returned when an empty 184 | * string is parsed. 185 | * 186 | * @param string $string 187 | * @return array 188 | */ 189 | public function parseCsv($string) 190 | { 191 | return array_filter(explode(',', $string)); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /tests/CallbackVisitorTest.php: -------------------------------------------------------------------------------- 1 | docCommentFactory = new FakeDocCommentFactory(); 21 | $this->file = m::mock('\Psecio\Parse\File'); 22 | } 23 | 24 | public function testCallback() 25 | { 26 | $node = new FakeNode(); 27 | 28 | $falseCheck = new FakeRule('failed'); 29 | 30 | $trueCheck = $this->getMockRule($node, true)->mock(); 31 | //$trueCheck = new FakeRule('passed', [$node]); 32 | 33 | $ruleCollection = $this->getMockCollection([$falseCheck, $trueCheck]); 34 | 35 | $visitor = new CallbackVisitor($ruleCollection, $this->docCommentFactory, false); 36 | 37 | $visitor->setFile($this->file); 38 | 39 | // Callback is called ONCE with failing check 40 | $this->assertFailureCalled(1, $falseCheck, $node, $visitor); 41 | } 42 | 43 | public function testIgnoreAnnotation() 44 | { 45 | $ruleName = 'dontIgnoreRule'; 46 | $node = new FakeNode('disable'); 47 | $rule = new FakeRule($ruleName, []); 48 | $ruleCollection = $this->getMockCollection([$rule]); 49 | $this->addDoc($node, [$ruleName], []); 50 | 51 | // The false means to ignore annotations 52 | $visitor = new CallbackVisitor($ruleCollection, $this->docCommentFactory, false); 53 | $visitor->setFile($this->file); 54 | 55 | // Callback is called once with failing check 56 | $this->assertFailureCalled(1, $rule, $node, $visitor); 57 | } 58 | 59 | public function testAnnotation() 60 | { 61 | $ruleName = 'ignoreRule'; 62 | $node = new FakeNode('disable'); 63 | $rule = new FakeRule($ruleName, []); 64 | $ruleCollection = $this->getMockCollection([$rule]); 65 | $this->addDoc($node, [$ruleName], []); 66 | 67 | // The true means to use annotations 68 | $visitor = new CallbackVisitor($ruleCollection, $this->docCommentFactory, true); 69 | $visitor->setFile($this->file); 70 | 71 | // Callback is called once with failing check 72 | $this->assertFailureCalled(0, $rule, $node, $visitor); 73 | } 74 | 75 | public function testAnnotationComment() 76 | { 77 | $ruleName = 'aRule'; 78 | $node = new FakeNode('disable'); 79 | $rule = new FakeRule($ruleName); 80 | $ruleCollection = $this->getMockCollection([$rule]); 81 | $block = new FakeDocComment('', [$ruleName . ' // ignore this'], []); 82 | $this->docCommentFactory->addDocComment($node->getDocComment(), $block); 83 | 84 | $visitor = new CallbackVisitor($ruleCollection, $this->docCommentFactory, true); 85 | $visitor->setFile($this->file); 86 | 87 | $this->assertFailureCalled(0, $rule, $node, $visitor); 88 | } 89 | 90 | public function testRuleWithSpaces() 91 | { 92 | // While having a rule with a space is not currently possible (it wouldn't 93 | // ever match normally), this makes sure the parser is allowing spaces before 94 | // the comment mark. 95 | $ruleName = 'a rule'; 96 | $node = new FakeNode('disable'); 97 | $rule = new FakeRule($ruleName, []); 98 | $ruleCollection = $this->getMockCollection([$rule]); 99 | $this->addDoc($node, [$ruleName], []); 100 | 101 | $visitor = new CallbackVisitor($ruleCollection, $this->docCommentFactory, true); 102 | $visitor->setFile($this->file); 103 | 104 | $this->assertFailureCalled(0, $rule, $node, $visitor); 105 | } 106 | 107 | public function testAnnotationTree() 108 | { 109 | $ruleName = 'rule1'; 110 | 111 | // Structure is: 112 | // node1 - disables rule 113 | // node2 - re-enables rule (flagged invalid) 114 | // node3 - doesn't set anything (disabled) 115 | // node4 - doesn't set anything (enabled) (flagged invalid) 116 | // All nodes fail rule 117 | $node1 = new FakeNode('disable'); 118 | $node2 = new FakeNode('enable'); 119 | $node3 = new FakeNode(); 120 | $node4 = new FakeNode(); 121 | 122 | $rule = new FakeRule($ruleName, []); 123 | $ruleCollection = $this->getMockCollection([$rule]); 124 | $this->addDoc($node1, [$ruleName], []); 125 | $this->addDoc($node2, [], [$ruleName]); 126 | 127 | $visitor = new CallbackVisitor($ruleCollection, $this->docCommentFactory, true); 128 | $visitor->setFile($this->file); 129 | 130 | $callList = []; 131 | $callback = function (RuleInterface $r, Node $n, File $f) use (&$callList) { 132 | $callList[] = [$r, $n, $f]; 133 | }; 134 | 135 | $expectedCallList = [ 136 | [$rule, $node2, $this->file], 137 | [$rule, $node4, $this->file], 138 | ]; 139 | 140 | $visitor->onNodeFailure($callback); 141 | 142 | $visitor->enterNode($node1); 143 | $visitor->enterNode($node2); 144 | $visitor->leaveNode($node2); 145 | $visitor->enterNode($node3); 146 | $visitor->leaveNode($node3); 147 | $visitor->leaveNode($node1); 148 | 149 | $visitor->enterNode($node4); 150 | $visitor->leaveNode($node4); 151 | 152 | $this->assertEquals($expectedCallList, $callList); 153 | } 154 | 155 | public function testEmptyDocBlock() 156 | { 157 | $ruleName = 'aRule'; 158 | 159 | $node = new FakeNode(); 160 | $falseCheck = new FakeRule($ruleName, []); 161 | $ruleCollection = $this->getMockCollection([$falseCheck]); 162 | 163 | // The true means to use annotations 164 | $visitor = new CallbackVisitor($ruleCollection, $this->docCommentFactory, true); 165 | $visitor->setFile($this->file); 166 | 167 | // Callback is called once with failing check 168 | $this->assertFailureCalled(1, $falseCheck, $node, $visitor); 169 | } 170 | 171 | private function addDoc($node, $disabled, $enabled) 172 | { 173 | $block = new FakeDocComment('', $disabled, $enabled); 174 | $this->docCommentFactory->addDocComment($node->getDocComment(), $block); 175 | } 176 | 177 | private function assertFailureCalled($times, RuleInterface $rule, 178 | Node $node, CallbackVisitor $visitor) 179 | { 180 | $callback = new MockeryCallableMock(); 181 | $callback->shouldBeCalled()->with($rule, $node, $this->file)->times($times); 182 | $visitor->onNodeFailure($callback); 183 | $visitor->enterNode($node); 184 | } 185 | 186 | private function getMockRule($node, $isValidReturns, $name = 'name') 187 | { 188 | $m = m::mock('\Psecio\Parse\RuleInterface') 189 | ->shouldReceive('getName') 190 | ->andReturn($name) 191 | ->zeroOrMoreTimes() 192 | ->shouldReceive('isValid') 193 | ->with($node) 194 | ->andReturn($isValidReturns); 195 | 196 | return $m; 197 | } 198 | 199 | private function getMockCollection($ruleList) 200 | { 201 | $ruleCollection = m::mock('\Psecio\Parse\RuleCollection') 202 | ->shouldReceive('getIterator') 203 | ->andReturn(new \ArrayIterator($ruleList)) 204 | ->mock(); 205 | 206 | return $ruleCollection; 207 | } 208 | } 209 | --------------------------------------------------------------------------------