├── 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 | [](https://packagist.org/packages/psecio/parse)
5 | [](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 |
--------------------------------------------------------------------------------