From aeff8447839b1dfa7d7cfe3366795f3ec59d4a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20J=C3=B8rgensen?= Date: Mon, 13 Jan 2020 15:45:59 +0100 Subject: [PATCH 1/3] Add security pull request from Dependabot --- README.md | 12 +- composer.json | 10 +- composer.lock | 132 +++++++++++++------ src/JiraIssue.php | 259 ------------------------------------- src/PullRequestIssue.php | 62 +++++++++ src/SecurityAlertIssue.php | 100 ++++++++++++++ src/SyncCommand.php | 164 ++++++++++++----------- 7 files changed, 352 insertions(+), 387 deletions(-) delete mode 100644 src/JiraIssue.php create mode 100644 src/PullRequestIssue.php create mode 100644 src/SecurityAlertIssue.php diff --git a/README.md b/README.md index dd56174..cff74d4 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,7 @@ It has some required and some optional settings, which are passed to the action - `JIRA_USER`: The ID of the Jira user which is associated with the 'JiraApiToken' secret, eg 'someuser@reload.dk' (**REQUIRED**) - `JIRA_PROJECT`: The project key for the Jira project where issues should be created, eg `TEST` or `ABC`. (**REQUIRED**) - `JIRA_ISSUE_TYPE`: Type of issue to create, e.g. `Security`. Defaults to `Bug`. (*Optional*) -- `JIRA_WATCHERS`: Jira users to add as watchers to tickets. Use the [YAML block scalar literal style indicator with stripping chomping indicator](https://yaml-multiline.info/) (pipe and dash: `|-`) to add multiple watchers. (*Optional*) -- `JIRA_RESTRICTED_GROUP`: If set, the action will add a restricted comment to the ticket, viewable by only this Jira group. (*Optional*) -- `JIRA_RESTRICTED_COMMENT`: The comment to post. Use the YAML multiline operator for adding linebreaks to the comment. (*Optional, but required if group is set*) +- `JIRA_WATCHERS`: Jira users to add as watchers to tickets. Separate multiple watchers with comma (no spaces). Here is an example setup which runs this action every 6 hours. @@ -54,13 +52,7 @@ jobs: JIRA_USER: someuser@reload.dk JIRA_PROJECT: ABC JIRA_ISSUE_TYPE: Security - JIRA_WATCHERS: |- - someuser@reload.dk - someotheruser@reload.dk - JIRA_RESTRICTED_GROUP: Developers - JIRA_RESTRICTED_COMMENT: |- - Remember to evaluate severity here and set ticket priority. - Check out the guide [in our wiki|https://foo.atlassian.net/wiki/]! + JIRA_WATCHERS: someuser@reload.dk,someotheruser@reload.dk ``` diff --git a/composer.json b/composer.json index 363acd9..7ab5ab3 100644 --- a/composer.json +++ b/composer.json @@ -4,11 +4,17 @@ "license": "MIT", "require": { "php": ">=7.2.0", - "lesstif/php-jira-rest-client": "^1", "softonic/graphql-client": "^1.2", "symfony/console": "^4", - "symfony/yaml": "^5.0" + "symfony/yaml": "^5.0", + "reload/jira-security-issue": "dev-master" }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/reload/jira-security-issue" + } + ], "autoload": { "psr-4": { "GitHubSecurityJira\\": "src/" diff --git a/composer.lock b/composer.lock index 91fb5e3..8298670 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1b41138cfb13498aaabe6ccf84dda64b", + "content-hash": "48620e04f863d75a9be1f26467312308", "packages": [ { "name": "appocular/coding-standard", @@ -308,24 +308,24 @@ }, { "name": "lesstif/php-jira-rest-client", - "version": "1.41.0", + "version": "1.43.0", "source": { "type": "git", "url": "https://github.com/lesstif/php-jira-rest-client.git", - "reference": "12e0e7c4723eb2d5952e8360c35c955fa64d1cf5" + "reference": "fd07dfa55e86b551d5c37c081184a49084844132" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lesstif/php-jira-rest-client/zipball/12e0e7c4723eb2d5952e8360c35c955fa64d1cf5", - "reference": "12e0e7c4723eb2d5952e8360c35c955fa64d1cf5", + "url": "https://api.github.com/repos/lesstif/php-jira-rest-client/zipball/fd07dfa55e86b551d5c37c081184a49084844132", + "reference": "fd07dfa55e86b551d5c37c081184a49084844132", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", - "monolog/monolog": "~1.12", + "monolog/monolog": "~1.12|^2.0", "netresearch/jsonmapper": "~0.11|^1.0", - "php": ">=5.5.9", + "php": ">=5.6.9", "vlucas/phpdotenv": "~1.0|~2.0|^3.0" }, "require-dev": { @@ -354,7 +354,7 @@ { "name": "KwangSeob Jeong", "email": "lesstif@gmail.com", - "homepage": "http://lesstif.com/" + "homepage": "https://lesstif.com/" } ], "description": "JIRA REST API Client for PHP Users.", @@ -364,25 +364,25 @@ "jira-rest", "rest" ], - "time": "2019-07-30T12:31:28+00:00" + "time": "2020-01-14T09:25:33+00:00" }, { "name": "monolog/monolog", - "version": "1.25.2", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "d5e2fb341cb44f7e2ab639d12a1e5901091ec287" + "reference": "c861fcba2ca29404dc9e617eedd9eff4616986b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/d5e2fb341cb44f7e2ab639d12a1e5901091ec287", - "reference": "d5e2fb341cb44f7e2ab639d12a1e5901091ec287", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c861fcba2ca29404dc9e617eedd9eff4616986b8", + "reference": "c861fcba2ca29404dc9e617eedd9eff4616986b8", "shasum": "" }, "require": { - "php": ">=5.3.0", - "psr/log": "~1.0" + "php": "^7.2", + "psr/log": "^1.0.1" }, "provide": { "psr/log-implementation": "1.0.0" @@ -390,33 +390,36 @@ "require-dev": { "aws/aws-sdk-php": "^2.4.9 || ^3.0", "doctrine/couchdb": "~1.0@dev", - "graylog2/gelf-php": "~1.0", - "jakub-onderka/php-parallel-lint": "0.9", + "elasticsearch/elasticsearch": "^6.0", + "graylog2/gelf-php": "^1.4.2", + "jakub-onderka/php-parallel-lint": "^0.9", "php-amqplib/php-amqplib": "~2.4", "php-console/php-console": "^3.1.3", - "phpunit/phpunit": "~4.5", - "phpunit/phpunit-mock-objects": "2.3.0", + "phpspec/prophecy": "^1.6.1", + "phpunit/phpunit": "^8.3", + "predis/predis": "^1.1", + "rollbar/rollbar": "^1.3", "ruflin/elastica": ">=0.90 <3.0", - "sentry/sentry": "^0.13", "swiftmailer/swiftmailer": "^5.3|^6.0" }, "suggest": { "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-mongo": "Allow sending log messages to a MongoDB server", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", "php-console/php-console": "Allow sending log messages to Google Chrome", "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server", - "sentry/sentry": "Allow sending log messages to a Sentry server" + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -442,7 +445,7 @@ "logging", "psr-3" ], - "time": "2019-11-13T10:00:05+00:00" + "time": "2019-12-20T14:22:59+00:00" }, { "name": "netresearch/jsonmapper", @@ -537,33 +540,34 @@ }, { "name": "phpoption/phpoption", - "version": "1.5.2", + "version": "1.7.2", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "2ba2586380f8d2b44ad1b9feb61c371020b27793" + "reference": "77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/2ba2586380f8d2b44ad1b9feb61c371020b27793", - "reference": "2ba2586380f8d2b44ad1b9feb61c371020b27793", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959", + "reference": "77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^5.5.9 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.7|^5.0" + "bamarni/composer-bin-plugin": "^1.3", + "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "1.7-dev" } }, "autoload": { - "psr-0": { - "PhpOption\\": "src/" + "psr-4": { + "PhpOption\\": "src/PhpOption/" } }, "notification-url": "https://packagist.org/downloads/", @@ -574,6 +578,10 @@ { "name": "Johannes M. Schmitt", "email": "schmittjoh@gmail.com" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com" } ], "description": "Option Type for PHP", @@ -583,7 +591,7 @@ "php", "type" ], - "time": "2019-11-06T22:27:00+00:00" + "time": "2019-12-15T19:35:24+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -864,6 +872,52 @@ "description": "A polyfill for getallheaders.", "time": "2019-03-08T08:55:37+00:00" }, + { + "name": "reload/jira-security-issue", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/reload/jira-security-issue.git", + "reference": "7f20393cc7a04e051832b4f9674837648e59da2a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reload/jira-security-issue/zipball/7f20393cc7a04e051832b4f9674837648e59da2a", + "reference": "7f20393cc7a04e051832b4f9674837648e59da2a", + "shasum": "" + }, + "require": { + "lesstif/php-jira-rest-client": "^1.42", + "php": ">=7.2.0" + }, + "require-dev": { + "appocular/coding-standard": "^1.0", + "phpstan/phpstan": "^0.12.4", + "phpunit/phpunit": "^8.5", + "sempro/phpunit-pretty-print": "^1.2", + "symfony/console": "^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Reload\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Reload\\": "tests/" + } + }, + "license": [ + "MIT" + ], + "description": "Create Jira issues if it doesn't exist", + "support": { + "source": "https://github.com/reload/jira-security-issue/tree/master", + "issues": "https://github.com/reload/jira-security-issue/issues" + }, + "time": "2020-01-09T13:28:51+00:00" + }, { "name": "slevomat/coding-standard", "version": "5.0.4", @@ -1118,7 +1172,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.13.0", + "version": "v1.13.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -1469,7 +1523,9 @@ "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "reload/jira-security-issue": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/JiraIssue.php b/src/JiraIssue.php deleted file mode 100644 index 47913ba..0000000 --- a/src/JiraIssue.php +++ /dev/null @@ -1,259 +0,0 @@ - 'Bug']; - - public function __construct( - string $jira_host, - string $jira_user, - string $jira_password, - string $jira_project, - string $github_repo, - string $package, - string $safeVersion, - string $vulnerableVersionRange - ) { - $this->jiraHost = $jira_host; - $this->jiraUser = $jira_user; - $this->jiraPassword = $jira_password; - $this->jiraProject = $jira_project; - $this->GitHubRepo = $github_repo; - $this->package = $package; - $this->safeVersion = $safeVersion; - $this->vulnerableVersionRange = $vulnerableVersionRange; - - $this->issueService = new IssueService(new ArrayConfiguration([ - 'jiraHost' => $this->jiraHost, - 'jiraUser' => $this->jiraUser, - 'jiraPassword' => $this->jiraPassword, - ])); - - $this->userService = new UserService(new ArrayConfiguration([ - 'jiraHost' => $this->jiraHost, - 'jiraUser' => $this->jiraUser, - 'jiraPassword' => $this->jiraPassword, - ])); - } - - /** - * Create the issue in Jira and set watchers and add a restricted comment. - * - * @return string|null - * The ticket id of the created issue. - */ - public function create(): ?string - { - $issueField = new IssueField(); - $issueField - ->setProjectKey($this->jiraProject) - ->setSummary("{$this->package} ({$this->safeVersion})") - ->setDescription($this->getBody()) - ->setIssueType($this->getField('issue_type')) - ->addLabel($this->GitHubRepo) - ->addLabel($this->package) - ->addLabel($this->getUniqueId()); - - // Create issue. - try { - $issue = $this->issueService->create($issueField); - } catch (\Throwable $t) { - echo "Could not create issue {$this->project}:{$this->safeVersion}: {$t->getMessage()}" . PHP_EOL; - return null; - } - - // Add watchers. - foreach ($this->getField('watchers') as $watchers_email) { - try { - $watchers_accountid = $this->getUserFieldFromEmail($watchers_email); - $this->issueService->addWatcher($issue->key, $watchers_accountid); - } catch (\Throwable $t) { - echo "Could not add watcher {$watchers_email} to issue {$issue->key}: {$t->getMessage()}" . PHP_EOL; - } - } - - // Set restricted comment. - if (!empty($this->getField('restricted_group')) && !empty($this->getField('restricted_comment'))) { - try { - $this->issueService->addComment($issue->key, $this->restrictedComment()); - } catch (\Throwable $t) { - echo "Could not add comment to issue {$issue->key}: {$t->getMessage()}" . PHP_EOL; - } - } - - return $issue->key; - } - - /** - * Look up an existing issue in Jira. - * - * This method performs a JQL search in Jira to determine whether the - * current issue already exists in this project. This is determined by - * looking at the repo name and the uniqueId label. - * - * @return string|null - * The ID of the issue found or null if no issue was found. - */ - public function existingIssue(): ?string - { - $jql = "PROJECT = '{$this->jiraProject}' " - . "AND labels IN ('{$this->GitHubRepo}') " - . "AND labels IN ('{$this->getUniqueId()}') " - . "ORDER BY created DESC"; - $result = $this->issueService->search($jql); - - if ($result->total > 0) { - return reset($result->issues)->key; - } - - return null; - } - - public function setField(string $field, $value) - { - $this->fields[$field] = $value; - } - - public function getField(string $field) - { - return $this->fields[$field] ?? null; - } - - public function addField(string $field, string $value) - { - $this->fields[$field][] = trim($value); - } - - /** - * Method to generate a unique id for this alert. - * - * It uses the package name, the manifest base path (ie not including - * 'composer.lock' or similar), and also the target version (safe version) - * to create a string that is safe to use as eg a Jira label. - * - * @return string - */ - protected function getUniqueId(): string - { - $strings[] = $this->package; - if (!empty($this->getField('manifest_path'))) { - $manifest_base_path = pathinfo($this->getField('manifest_path'), PATHINFO_DIRNAME); - if (!empty($manifest_base_path) && $manifest_base_path != '.') { - $strings[] = $manifest_base_path; - } - } - $strings[] = $this->safeVersion; - return implode(':', $strings); - } - - /** - * Return formatted body string. - * - * @return string - */ - protected function getBody(): string - { - $advisory_description = wordwrap($this->getField('advisory_description'), 100); - $references = implode(', ', $this->getField('references')); - $ecosystem = $this->getField('ecosystem') ? '(' . $this->getField('ecosystem') . ')' : ''; - return <<GitHubRepo}|https://github.com/{$this->GitHubRepo}] -- Package: {$this->package} $ecosystem -- Severity: {$this->getField('severity')} -- Vulnerable version: {$this->vulnerableVersionRange} -- Secure version: {$this->safeVersion} -- Links: {$references} -{noformat} -{$advisory_description} -{noformat} -EOT; - } - - /** - * Return array containing formatted values for restricted comment. - * - * @return array - * The array containing settings and text for restricted comment. - */ - protected function restrictedComment(): array - { - return [ - 'visibility' => [ - 'type' => 'role', - 'value' => $this->getField('restricted_group'), - ], - 'body' => $this->getField('restricted_comment') . PHP_EOL - . $this->formatWatchers($this->getField('watchers')), - ]; - } - - protected function formatWatchers(array $watchers): string - { - if (empty($watchers)) { - return ''; - } - - $watchers_keys = array_map(function (string $watchers_email) { - return $this->getUserFieldFromEmail($watchers_email, 'key'); - }, $watchers); - return 'Watchers: [~' . implode('], [~', $watchers_keys) . '].'; - } - - /** - * Helper method to lookup a user in Jira. - * - * @param string $email - * The email address to lookup. - * @param string $user_field - * The user field to return. Optional. Defaults to accountId. - * - * @return string|null - * The user field value. - */ - protected function getUserFieldFromEmail(string $email, string $user_field = 'accountId'): ?string - { - try { - $paramArray = [ - 'query' => $email, - 'project' => $this->jiraProject, - 'maxResults' => 1 - ]; - - $users = $this->userService->findAssignableUsers($paramArray); - - if (empty($users)) { - return null; - } - } catch (JiraException $e) { - echo "ERROR: Could not query Jira with email '${email}'. " . $e->getMessage(); - return null; - } - - $user = array_pop($users); - return $user->$user_field; - } -} diff --git a/src/PullRequestIssue.php b/src/PullRequestIssue.php new file mode 100644 index 0000000..42e402b --- /dev/null +++ b/src/PullRequestIssue.php @@ -0,0 +1,62 @@ + $data + */ + public function __construct(array $data) + { + $this->package = \preg_replace('/.*Bump (.*) from.*/', '$1', $data['title']) ?? ''; + $this->manifestPath = \preg_replace('/.* in \/(.*)/', '$1', $data['title']) ?? ''; + $this->safeVersion = \preg_replace('/.*to (.*) in.*/', '$1', $data['title']) ?? ''; + + $githubRepo = \getenv('GITHUB_REPOSITORY') ?: ''; + + $body = <<package} +- Secure version: {$this->safeVersion} +- Pull request with more info: [#{$data['number']}|{$data['url']}] +EOT; + + parent::__construct(); + + $this->setKeyLabel($githubRepo); + $this->setKeyLabel($this->uniqueId()); + $this->setTitle("{$this->package} ({$this->safeVersion})"); + $this->setBody($body); + } + + /** + * The unique ID of the severity. + * + * @return string + */ + public function uniqueId(): string + { + return "{$this->package}:{$this->manifestPath}:{$this->safeVersion}"; + } +} diff --git a/src/SecurityAlertIssue.php b/src/SecurityAlertIssue.php new file mode 100644 index 0000000..57ec423 --- /dev/null +++ b/src/SecurityAlertIssue.php @@ -0,0 +1,100 @@ + $data + */ + public function __construct(array $data) + { + // phpcs:enable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint + $this->package = $data['securityVulnerability']['package']['name']; + $this->safeVersion = $data['securityVulnerability']['firstPatchedVersion']['identifier']; + $this->vulnerableVersionRange = $data['securityVulnerability']['vulnerableVersionRange']; + $this->manifestPath = \pathinfo($data['vulnerableManifestPath'], \PATHINFO_DIRNAME); + + $references = []; + + foreach ($data['securityVulnerability']['advisory']['references'] as $ref) { + if (!\array_key_exists('url', $ref) || !\is_string($ref['url'])) { + continue; + } + + $references[] = $ref['url']; + } + + $advisory_description = \wordwrap($data['securityVulnerability']['advisory']['description'] ?? '', 100); + $ecosystem = $data['securityVulnerability']['package']['ecosystem'] ?? ''; + $githubRepo = \getenv('GITHUB_REPOSITORY') ?: ''; + + $body = <<package} ($ecosystem) +- Vulnerable version: {$this->vulnerableVersionRange} +- Secure version: {$this->safeVersion} + +EOT; + + if (\is_array($references) && (\count($references) > 0)) { + $body .= "- Links: \n-- " . \implode("\n-- ", $references); + } + + $body .= <<setKeyLabel($githubRepo); + $this->setKeyLabel($this->uniqueId()); + $this->setTitle("{$this->package} ({$this->safeVersion})"); + $this->setBody($body); + } + + /** + * The unique ID of the severity. + * + * @return string + */ + public function uniqueId(): string + { + if ($this->manifestPath === '.') { + return "{$this->package}:{$this->safeVersion}"; + } + + return "{$this->package}:{$this->manifestPath}:{$this->safeVersion}"; + } +} diff --git a/src/SyncCommand.php b/src/SyncCommand.php index 39657c7..e001943 100644 --- a/src/SyncCommand.php +++ b/src/SyncCommand.php @@ -73,92 +73,53 @@ protected function initialize(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output) { - $jira_host = getenv('JIRA_HOST'); - $jira_user = getenv('JIRA_USER'); - $jira_password = getenv('JIRA_TOKEN'); - $jira_project = getenv('JIRA_PROJECT'); - - $github_repo = getenv('GITHUB_REPOSITORY'); - - $issue_type = getenv('JIRA_ISSUE_TYPE'); - - $watchers = []; - if (is_string(getenv('JIRA_WATCHERS'))) { - $watchers = explode("\n", getenv('JIRA_WATCHERS')) ?? []; - } - - $res_group = getenv('JIRA_RESTRICTED_GROUP'); - $res_comment = getenv('JIRA_RESTRICTED_COMMENT'); - // Fetch alert data from GitHub. $alerts = $this->fetchAlertData(); if (empty($alerts)) { - $this->logLine($output, 'No alerts found.'); + $this->log($output, 'No alerts found.'); } + $alertsFound = []; + // Go through each alert and create a Jira issue if one does not exist. foreach ($alerts as $alert) { - $package = $alert['securityVulnerability']['package']['name']; - $safeVersion = $alert['securityVulnerability']['firstPatchedVersion']['identifier']; - $vulnerableVersionRange = $alert['securityVulnerability']['vulnerableVersionRange']; - - $issue = new JiraIssue( - $jira_host, - $jira_user, - $jira_password, - $jira_project, - $github_repo, - $package, - $safeVersion, - $vulnerableVersionRange - ); + $issue = new SecurityAlertIssue($alert); - $issue->setField('severity', $alert['securityVulnerability']['severity'] ?? ''); - $issue->setField('ecosystem', $alert['securityVulnerability']['package']['ecosystem'] ?? ''); - $issue->setField('advisory_description', $alert['securityVulnerability']['advisory']['description'] ?? ''); - $issue->setField('manifest_path', $alert['vulnerableManifestPath']); - foreach ($alert['securityVulnerability']['advisory']['references'] as $ref) { - if (!empty($ref['url'])) { - $issue->addField('references', $ref['url']); - } - } + $existingKey = $issue->exists(); - if (!empty($issue_type)) { - $issue->setField('issue_type', $issue_type); - } - $issue->setField('watchers', $watchers); - $issue->setField('restricted_group', $res_group ?? ''); - $issue->setField('restricted_comment', $res_comment ?? []); - - $timestamp = gmdate(DATE_ISO8601); - $this->log($output, "{$timestamp} - {$jira_project} - {$package}:{$vulnerableVersionRange} - "); - - // Determine whether there is an issue for this alert already. - try { - $key = $issue->existingIssue(); - } catch (\Throwable $t) { - $this->logLine($output, "ERROR ACCESSING JIRA: {$t->getMessage()}."); - exit(1); + if (!is_null($existingKey)) { + $this->log($output, "Existing issue {$existingKey} covers {$issue->uniqueId()}."); + } elseif (!$input->getOption('dry-run')) { + $key = $issue->ensure(); + $this->log($output, "Created issue {$key} for {$issue->uniqueId()}."); + } else { + $this->log($output, "Would have created an issue for {$issue->uniqueId()} if not a dry run."); } - if ($key) { - $this->logLine($output, "Existing issue found: {$key}."); + + $alertsFound[] = $issue->uniqueId(); + } + + $pull_requests = $this->fetchPullRequestData(); + + foreach ($pull_requests as $pull_request) { + $issue = new PullRequestIssue($pull_request['node']); + + if (in_array($issue->uniqueId(), $alertsFound)) { continue; } - // Create the Jira issue. - if (empty($input->getOption('dry-run'))) { - $key = $issue->create(); + $existingKey = $issue->exists(); - // Issue creation failed. Bail out. - if (empty($key)) { - $this->logLine($output, 'ERROR CREATING ISSUE.'); - exit(1); - } - $this->logLine($output, "Created issue {$key}"); + if (!is_null($existingKey)) { + $this->log($output, "Existing issue {$existingKey} covers {$issue->uniqueId()}."); + } elseif (!$input->getOption('dry-run')) { + $key = $issue->ensure(); + $this->log($output, "Created issue {$key} for {$issue->uniqueId()}."); } else { - $this->logLine($output, "Would have created an issue in {$jira_project} if not a dry run."); + $this->log($output, "Would have created an issue for {$issue->uniqueId()} if not a dry run."); } } + } /** @@ -231,6 +192,58 @@ protected function fetchAlertData() return $alerts; } + /** + * Fetch Dependabot pull request data from GitHub. + * + * @return array>> + */ + protected function fetchPullRequestData(): array + { + $repo = \getenv('GITHUB_REPOSITORY'); + $author = 'author:app/dependabot author:app/dependabot-preview'; + + $query = <<getGHClient()->query($query, $variables); + + if ($response->hasErrors()) { + $messages = \array_map(static function (array $error) { + return $error['message']; + }, $response->getErrors()); + + throw new RuntimeException( + \sprintf('GraphQL client error: %s. Original query: %s', \implode(', ', $messages), $query), + ); + } + + // Drill down to the response data we want, if there. + $pr_data = $response->getData(); + $prs = $pr_data['search']['edges'] ?? []; + + return $pr_data['search']['edges'] ?? []; + } + /** * Create the GraphQL client with supplied Bearer token. * @@ -273,15 +286,10 @@ protected function log(OutputInterface $output, string $message) return; } - $output->write($message); - } - - protected function logLine(OutputInterface $output, string $message) - { - if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) { - return; - } + $timestamp = gmdate(DATE_ISO8601); + $jira_project = getenv('JIRA_PROJECT'); - $output->writeln($message); + $output->writeln("{$timestamp} - {$jira_project} - {$message}"); } + } From 44c5b1560d49340da9f3db0aaf993bcdb3f2d8c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20J=C3=B8rgensen?= Date: Wed, 15 Jan 2020 11:04:32 +0100 Subject: [PATCH 2/3] Cleanup code style * phpcs (rules based on @xendk's appocular standard) * phpstan * markdownlint --- .phpcs.xml | 17 ++ Dockerfile | 4 +- README.md | 9 +- composer.json | 16 +- composer.lock | 410 ++++++++++++++++++++++++-------------------- phpstan.neon | 5 + src/SyncCommand.php | 81 +++++---- 7 files changed, 310 insertions(+), 232 deletions(-) create mode 100644 .phpcs.xml create mode 100644 phpstan.neon diff --git a/.phpcs.xml b/.phpcs.xml new file mode 100644 index 0000000..15fce7a --- /dev/null +++ b/.phpcs.xml @@ -0,0 +1,17 @@ + + + ./src + ./tests + + + + + + + + + + + + diff --git a/Dockerfile b/Dockerfile index b4e9447..c6a635b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # ----------------- -FROM composer:1.9 AS build-env +FROM composer:1.9.1 AS build-env COPY . /opt/ghsec-jira/ @@ -8,7 +8,7 @@ WORKDIR /opt/ghsec-jira RUN composer install --prefer-dist --no-dev # ----------------- -FROM php:7.3.12-alpine +FROM php:7.4.1-alpine COPY --from=build-env /opt/ghsec-jira/ /opt/ghsec-jira/ diff --git a/README.md b/README.md index cff74d4..45a65de 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # github-security-jira -GitHub Action for mapping security alerts to Jira tickets. +GitHub Action for mapping security alerts to Jira tickets. ## Setup @@ -9,22 +9,22 @@ You need the following pieces set up to sync alerts with Jira: 1. Two repo secrets containing a GitHub access token and a Jira API token, respectively. 2. A workflow file which runs the action on a schedule, continually creating new tickets when necessary. - ### Repo secrets + The `reload/github-security-jira` action requires you to [create two encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets#creating-encrypted-secrets) in the repo: 1. A secret called `GitHubSecurityToken` which should contain a [Personal Access Token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) for the GitHub user under which this action should be executed. The token must include the `public_repo` scope if checking only public repos, or the `repo` scope for use on private repos. Also, the user must have [access to security alerts in the repo](https://help.github.com/en/github/managing-security-vulnerabilities/managing-alerts-for-vulnerable-dependencies-in-your-organization). 2. A secret called `JiraApiToken` containing an [API Token](https://confluence.atlassian.com/cloud/api-tokens-938839638.html) for the Jira user that should be used to create tickets. - ### Workflow file setup + The [GitHub workflow file](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/configuring-a-workflow#creating-a-workflow-file) should reside in any repo where you want to sync security alerts with Jira. It has some required and some optional settings, which are passed to the action as environment variables: - `GH_SECURITY_TOKEN`: A reference to the repo secret `GitHubSecurityToken` (**REQUIRED**) - `JIRA_TOKEN`: A reference to the repo secret `JiraApiToken` (**REQUIRED**) -- `JIRA_HOST`: The endpoint for your Jira instance, e.g. https://foo.atlassian.net (**REQUIRED**) +- `JIRA_HOST`: The endpoint for your Jira instance, e.g. (**REQUIRED**) - `JIRA_USER`: The ID of the Jira user which is associated with the 'JiraApiToken' secret, eg 'someuser@reload.dk' (**REQUIRED**) - `JIRA_PROJECT`: The project key for the Jira project where issues should be created, eg `TEST` or `ABC`. (**REQUIRED**) - `JIRA_ISSUE_TYPE`: Type of issue to create, e.g. `Security`. Defaults to `Bug`. (*Optional*) @@ -55,7 +55,6 @@ jobs: JIRA_WATCHERS: someuser@reload.dk,someotheruser@reload.dk ``` - ## Local development Copy `docker-composer.override.example.yml` to `docker-composer.override.yml` and edit according to your settings. diff --git a/composer.json b/composer.json index 7ab5ab3..0a417b4 100644 --- a/composer.json +++ b/composer.json @@ -10,14 +10,22 @@ "reload/jira-security-issue": "dev-master" }, "repositories": [ - { - "type": "vcs", - "url": "https://github.com/reload/jira-security-issue" - } + { + "type": "vcs", + "url": "https://github.com/appocular/coding-standard" + }, + { + "type": "vcs", + "url": "https://github.com/reload/jira-security-issue" + } ], "autoload": { "psr-4": { "GitHubSecurityJira\\": "src/" } + }, + "require-dev": { + "appocular/coding-standard": "^1.0", + "phpstan/phpstan": "^0.12.5" } } diff --git a/composer.lock b/composer.lock index 8298670..e240d14 100644 --- a/composer.lock +++ b/composer.lock @@ -4,53 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "48620e04f863d75a9be1f26467312308", + "content-hash": "c9ead59112aebd7e19abc06fc7bb932e", "packages": [ - { - "name": "appocular/coding-standard", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/appocular/coding-standard.git", - "reference": "84cc48dc7552b8452e44189860e9db7ea5c4fd85" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/appocular/coding-standard/zipball/84cc48dc7552b8452e44189860e9db7ea5c4fd85", - "reference": "84cc48dc7552b8452e44189860e9db7ea5c4fd85", - "shasum": "" - }, - "require": { - "slevomat/coding-standard": "^5.0", - "squizlabs/php_codesniffer": "^3.5" - }, - "type": "project", - "autoload": { - "psr-4": { - "Appocular\\CodingStandard\\": "AppocularCodingStandard" - } - }, - "license": [ - "MIT" - ], - "description": "Appocular PHP coding standard, PHP Code Sniffer rules.", - "keywords": [ - "CodeSniffer", - "PHPCodeSniffer", - "coding", - "coding standard", - "cs", - "phpcs", - "ruleset", - "sniffer", - "standard" - ], - "support": { - "source": "https://github.com/appocular/coding-standard/tree/1.0.3", - "issues": "https://github.com/appocular/coding-standard/issues" - }, - "time": "2019-11-17T18:59:56+00:00" - }, { "name": "guzzlehttp/guzzle", "version": "6.4.1", @@ -593,53 +548,6 @@ ], "time": "2019-12-15T19:35:24+00:00" }, - { - "name": "phpstan/phpdoc-parser", - "version": "0.3.5", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "8c4ef2aefd9788238897b678a985e1d5c8df6db4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/8c4ef2aefd9788238897b678a985e1d5c8df6db4", - "reference": "8c4ef2aefd9788238897b678a985e1d5c8df6db4", - "shasum": "" - }, - "require": { - "php": "~7.1" - }, - "require-dev": { - "consistence/coding-standard": "^3.5", - "jakub-onderka/php-parallel-lint": "^0.9.2", - "phing/phing": "^2.16.0", - "phpstan/phpstan": "^0.10", - "phpunit/phpunit": "^6.3", - "slevomat/coding-standard": "^4.7.2", - "squizlabs/php_codesniffer": "^3.3.2", - "symfony/process": "^3.4 || ^4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.3-dev" - } - }, - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "time": "2019-06-07T19:13:52+00:00" - }, { "name": "psr/cache", "version": "1.0.1", @@ -918,46 +826,6 @@ }, "time": "2020-01-09T13:28:51+00:00" }, - { - "name": "slevomat/coding-standard", - "version": "5.0.4", - "source": { - "type": "git", - "url": "https://github.com/slevomat/coding-standard.git", - "reference": "287ac3347c47918c0bf5e10335e36197ea10894c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/287ac3347c47918c0bf5e10335e36197ea10894c", - "reference": "287ac3347c47918c0bf5e10335e36197ea10894c", - "shasum": "" - }, - "require": { - "php": "^7.1", - "phpstan/phpdoc-parser": "^0.3.1", - "squizlabs/php_codesniffer": "^3.4.1" - }, - "require-dev": { - "jakub-onderka/php-parallel-lint": "1.0.0", - "phing/phing": "2.16.1", - "phpstan/phpstan": "0.11.4", - "phpstan/phpstan-phpunit": "0.11", - "phpstan/phpstan-strict-rules": "0.11", - "phpunit/phpunit": "8.0.5" - }, - "type": "phpcodesniffer-standard", - "autoload": { - "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", - "time": "2019-03-22T19:10:53+00:00" - }, { "name": "softonic/graphql-client", "version": "1.2.0", @@ -1043,57 +911,6 @@ ], "time": "2019-08-07T08:34:15+00:00" }, - { - "name": "squizlabs/php_codesniffer", - "version": "3.5.2", - "source": { - "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "65b12cdeaaa6cd276d4c3033a95b9b88b12701e7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/65b12cdeaaa6cd276d4c3033a95b9b88b12701e7", - "reference": "65b12cdeaaa6cd276d4c3033a95b9b88b12701e7", - "shasum": "" - }, - "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" - }, - "bin": [ - "bin/phpcs", - "bin/phpcbf" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Greg Sherwood", - "role": "lead" - } - ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", - "keywords": [ - "phpcs", - "standards" - ], - "time": "2019-10-28T04:36:32+00:00" - }, { "name": "symfony/console", "version": "v4.4.0", @@ -1520,7 +1337,230 @@ "time": "2019-09-10T21:37:39+00:00" } ], - "packages-dev": [], + "packages-dev": [ + { + "name": "appocular/coding-standard", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/appocular/coding-standard.git", + "reference": "84cc48dc7552b8452e44189860e9db7ea5c4fd85" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/appocular/coding-standard/zipball/84cc48dc7552b8452e44189860e9db7ea5c4fd85", + "reference": "84cc48dc7552b8452e44189860e9db7ea5c4fd85", + "shasum": "" + }, + "require": { + "slevomat/coding-standard": "^5.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "project", + "autoload": { + "psr-4": { + "Appocular\\CodingStandard\\": "AppocularCodingStandard" + } + }, + "license": [ + "MIT" + ], + "description": "Appocular PHP coding standard, PHP Code Sniffer rules.", + "keywords": [ + "CodeSniffer", + "PHPCodeSniffer", + "coding", + "coding standard", + "cs", + "phpcs", + "ruleset", + "sniffer", + "standard" + ], + "support": { + "source": "https://github.com/appocular/coding-standard/tree/1.0.3", + "issues": "https://github.com/appocular/coding-standard/issues" + }, + "time": "2019-11-17T18:59:56+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "0.3.5", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "8c4ef2aefd9788238897b678a985e1d5c8df6db4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/8c4ef2aefd9788238897b678a985e1d5c8df6db4", + "reference": "8c4ef2aefd9788238897b678a985e1d5c8df6db4", + "shasum": "" + }, + "require": { + "php": "~7.1" + }, + "require-dev": { + "consistence/coding-standard": "^3.5", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "phing/phing": "^2.16.0", + "phpstan/phpstan": "^0.10", + "phpunit/phpunit": "^6.3", + "slevomat/coding-standard": "^4.7.2", + "squizlabs/php_codesniffer": "^3.3.2", + "symfony/process": "^3.4 || ^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.3-dev" + } + }, + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "time": "2019-06-07T19:13:52+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "0.12.5", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "71a20c18f06c53605251a00a8efe443fa85225d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/71a20c18f06c53605251a00a8efe443fa85225d1", + "reference": "71a20c18f06c53605251a00a8efe443fa85225d1", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "time": "2020-01-12T14:31:21+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "5.0.4", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "287ac3347c47918c0bf5e10335e36197ea10894c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/287ac3347c47918c0bf5e10335e36197ea10894c", + "reference": "287ac3347c47918c0bf5e10335e36197ea10894c", + "shasum": "" + }, + "require": { + "php": "^7.1", + "phpstan/phpdoc-parser": "^0.3.1", + "squizlabs/php_codesniffer": "^3.4.1" + }, + "require-dev": { + "jakub-onderka/php-parallel-lint": "1.0.0", + "phing/phing": "2.16.1", + "phpstan/phpstan": "0.11.4", + "phpstan/phpstan-phpunit": "0.11", + "phpstan/phpstan-strict-rules": "0.11", + "phpunit/phpunit": "8.0.5" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "time": "2019-03-22T19:10:53+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.5.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "65b12cdeaaa6cd276d4c3033a95b9b88b12701e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/65b12cdeaaa6cd276d4c3033a95b9b88b12701e7", + "reference": "65b12cdeaaa6cd276d4c3033a95b9b88b12701e7", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2019-10-28T04:36:32+00:00" + } + ], "aliases": [], "minimum-stability": "stable", "stability-flags": { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..e610f19 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: max + excludes_analyse: + - vendor + - tests diff --git a/src/SyncCommand.php b/src/SyncCommand.php index e001943..4d39cea 100644 --- a/src/SyncCommand.php +++ b/src/SyncCommand.php @@ -4,13 +4,13 @@ namespace GitHubSecurityJira; +use RuntimeException; +use Softonic\GraphQL\Client as GraphQLClient; +use Softonic\GraphQL\ClientBuilder; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Yaml\Yaml; -use Softonic\GraphQL\ClientBuilder; /** * The default sync command. @@ -25,6 +25,11 @@ class SyncCommand extends Command */ protected static $defaultName = 'sync'; + /** + * Required options in environment variables. + * + * @var array + */ protected $requiredOptions = [ 'GITHUB_REPOSITORY', 'GH_SECURITY_TOKEN', @@ -45,7 +50,7 @@ public function __construct() /** * {@inheritDoc} */ - protected function configure() + protected function configure(): void { $this ->setDescription('Sync GitHub Alert status to Jira') @@ -54,14 +59,16 @@ protected function configure() 'dry-run', null, InputOption::VALUE_NONE, - 'Do dry run (dont change anything)' + 'Do dry run (dont change anything)', ); } /** * {@inheritDoc} + * + * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter */ - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { // Validate config. $this->validateConfig(); @@ -75,7 +82,8 @@ protected function execute(InputInterface $input, OutputInterface $output) // Fetch alert data from GitHub. $alerts = $this->fetchAlertData(); - if (empty($alerts)) { + + if (!\is_array($alerts)) { $this->log($output, 'No alerts found.'); } @@ -87,7 +95,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $existingKey = $issue->exists(); - if (!is_null($existingKey)) { + if (!\is_null($existingKey)) { $this->log($output, "Existing issue {$existingKey} covers {$issue->uniqueId()}."); } elseif (!$input->getOption('dry-run')) { $key = $issue->ensure(); @@ -104,13 +112,13 @@ protected function execute(InputInterface $input, OutputInterface $output) foreach ($pull_requests as $pull_request) { $issue = new PullRequestIssue($pull_request['node']); - if (in_array($issue->uniqueId(), $alertsFound)) { + if (\in_array($issue->uniqueId(), $alertsFound)) { continue; } $existingKey = $issue->exists(); - if (!is_null($existingKey)) { + if (!\is_null($existingKey)) { $this->log($output, "Existing issue {$existingKey} covers {$issue->uniqueId()}."); } elseif (!$input->getOption('dry-run')) { $key = $issue->ensure(); @@ -120,13 +128,19 @@ protected function execute(InputInterface $input, OutputInterface $output) } } + return 0; } /** + * phpcs:disable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint + * * Fetch alert data from GitHub. + * + * @return array */ - protected function fetchAlertData() + protected function fetchAlertData(): array { + // phpcs:enable SlevomatCodingStandard.TypeHints.DisallowMixedTypeHint.DisallowedMixedTypeHint $query = <<<'GQL' query alerts($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { @@ -168,28 +182,28 @@ protected function fetchAlertData() } GQL; - $repo = explode('/', getenv('GITHUB_REPOSITORY')); + $repo = \explode('/', \getenv('GITHUB_REPOSITORY') ?: ''); $variables = [ 'owner' => $repo[0], 'repo' => $repo[1], ]; $response = $this->getGHClient()->query($query, $variables); + if ($response->hasErrors()) { - $messages = array_map(function (array $error) { + $messages = \array_map(static function (array $error) { return $error['message']; }, $response->getErrors()); - throw new \RuntimeException( - sprintf('GraphQL client error: %s. Original query: %s', implode(', ', $messages), $query) + throw new RuntimeException( + \sprintf('GraphQL client error: %s. Original query: %s', \implode(', ', $messages), $query), ); } // Drill down to the response data we want, if there. $alert_data = $response->getData(); - $alerts = $alert_data['repository']['vulnerabilityAlerts']['nodes'] ?? []; - return $alerts; + return $alert_data['repository']['vulnerabilityAlerts']['nodes'] ?? []; } /** @@ -239,57 +253,52 @@ protected function fetchPullRequestData(): array // Drill down to the response data we want, if there. $pr_data = $response->getData(); - $prs = $pr_data['search']['edges'] ?? []; return $pr_data['search']['edges'] ?? []; } /** * Create the GraphQL client with supplied Bearer token. - * */ - protected function getGHClient() + protected function getGHClient(): GraphQLClient { + $access_token = \getenv('GH_SECURITY_TOKEN'); - $access_token = getenv('GH_SECURITY_TOKEN'); - $client = ClientBuilder::build('https://api.github.com/graphql', [ + return ClientBuilder::build('https://api.github.com/graphql', [ 'headers' => [ 'Accept' => 'application/json', 'Authorization' => "Bearer {$access_token}", ], ]); - - return $client; } /** * Validate the required options. */ - protected function validateConfig() + protected function validateConfig(): void { foreach ($this->requiredOptions as $option) { - $var = getenv($option); - if ($var === false || empty($var)) { - throw new \RuntimeException("Required env variable '{$option}' not set or empty."); + $var = \getenv($option); + + if (!\is_string($var)) { + throw new RuntimeException("Required env variable '{$option}' not set or empty."); } - if ($option == 'GITHUB_REPOSITORY') { - if (count(explode('/', $var)) < 2) { - throw new \RuntimeException('GitHub repository invalid: ' . getenv('GITHUB_REPOSITORY')); - } + + if (($option === 'GITHUB_REPOSITORY') && (\count(\explode('/', $var)) < 2)) { + throw new RuntimeException('GitHub repository invalid: ' . \getenv('GITHUB_REPOSITORY')); } } } - protected function log(OutputInterface $output, string $message) + protected function log(OutputInterface $output, string $message): void { if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) { return; } - $timestamp = gmdate(DATE_ISO8601); - $jira_project = getenv('JIRA_PROJECT'); + $timestamp = \gmdate(\DATE_ISO8601); + $jira_project = \getenv('JIRA_PROJECT'); $output->writeln("{$timestamp} - {$jira_project} - {$message}"); } - } From 873c1af46601c5a3732dcb42d41ad5121326f0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arne=20J=C3=B8rgensen?= Date: Thu, 16 Jan 2020 09:18:46 +0100 Subject: [PATCH 3/3] Add Makefile for running linters etc. --- .mdlrc | 2 ++ Makefile | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 .mdlrc create mode 100644 Makefile diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 0000000..60ecd7f --- /dev/null +++ b/.mdlrc @@ -0,0 +1,2 @@ +# Disable "MD013 Line length" and "MD029 Ordered list item prefix". +rules "~MD013", "~MD029" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1950f8a --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +.PHONY: check phpstan phpcs markdownlint + +check: phpstan phpcs markdownlint + +phpstan: + -vendor/bin/phpstan analyse . + +phpcs: + -vendor/bin/phpcs -s bin/ src/ + +# gem install mdl +markdownlint: + -mdl *.md