From ac371e2a37c5340ad8055406878bfdff24c2fffe Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Sun, 29 Jul 2018 06:35:25 +0200
Subject: [PATCH] feat(github): vulnerability alerts (#2321)

Adds rules to skip any configured grouping or schedules that prevent insecure packages from being updated immediately.

If GitHub's vulnerability alerts are detected, package rules are added to force empty schedule and grouping for each affected package. Settings are configurable via new `vulnerabilityAlerts` config object, e.g. so that custom PR titles, labels or assignees can be configured.

Closes #1567
---
 lib/config/definitions.js                     | 13 +++++++
 lib/workers/repository/init/index.js          |  3 +-
 lib/workers/repository/init/vulnerability.js  | 38 +++++++++++++++++++
 .../__snapshots__/vulnerability.spec.js.snap  | 19 ++++++++++
 .../repository/init/vulnerability.spec.js     | 37 ++++++++++++++++++
 .../__snapshots__/flatten.spec.js.snap        | 20 ++++++++++
 website/docs/configuration-options.md         | 23 +++++++++++
 7 files changed, 152 insertions(+), 1 deletion(-)
 create mode 100644 lib/workers/repository/init/vulnerability.js
 create mode 100644 test/workers/repository/init/__snapshots__/vulnerability.spec.js.snap
 create mode 100644 test/workers/repository/init/vulnerability.spec.js

diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 5f0b8719ba..5c177150a3 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -686,6 +686,19 @@ const options = [
     cli: false,
     env: false,
   },
+  {
+    name: 'vulnerabilityAlerts',
+    description:
+      'Config to apply when Renovate detects a PR is necessary due to vulnerability of existing package version.',
+    type: 'object',
+    default: {
+      groupName: null,
+      schedule: [],
+      commitMessageSuffix: '[SECURITY]',
+    },
+    cli: false,
+    env: false,
+  },
   // Default templates
   {
     name: 'branchName',
diff --git a/lib/workers/repository/init/index.js b/lib/workers/repository/init/index.js
index 80036f44d8..8b5b6460af 100644
--- a/lib/workers/repository/init/index.js
+++ b/lib/workers/repository/init/index.js
@@ -4,6 +4,7 @@ const { initApis } = require('../init/apis');
 const { checkBaseBranch } = require('./base');
 const { mergeRenovateConfig } = require('./config');
 const { detectSemanticCommits } = require('./semantic');
+const { detectVulnerabilityAlerts } = require('./vulnerability');
 
 async function initRepo(input) {
   let config = {
@@ -19,7 +20,7 @@ async function initRepo(input) {
   checkIfConfigured(config);
   config = await checkBaseBranch(config);
   config.semanticCommits = await detectSemanticCommits(config);
-  await platform.getVulnerabilityAlerts();
+  config = await detectVulnerabilityAlerts(config);
   return config;
 }
 
diff --git a/lib/workers/repository/init/vulnerability.js b/lib/workers/repository/init/vulnerability.js
new file mode 100644
index 0000000000..1b68304741
--- /dev/null
+++ b/lib/workers/repository/init/vulnerability.js
@@ -0,0 +1,38 @@
+module.exports = {
+  detectVulnerabilityAlerts,
+};
+
+async function detectVulnerabilityAlerts(input) {
+  if (!(input && input.vulnerabilityAlerts)) {
+    return input;
+  }
+  if (input.vulnerabilityAlerts.enabled === false) {
+    return input;
+  }
+  const alerts = await platform.getVulnerabilityAlerts();
+  if (!alerts.length) {
+    return input;
+  }
+  const config = { ...input };
+  const alertPackageRules = alerts
+    .map(alert => {
+      if (!alert.fixedIn) {
+        logger.warn({ alert }, 'Vulnerability alert has no fixedIn version');
+        return null;
+      }
+      const rule = {};
+      rule.packageNames = [alert.packageName];
+      // Raise only for where the currentVersion is vulnerable
+      rule.matchCurrentVersion = `< ${alert.fixedIn}`;
+      // Don't propose upgrades to any versions that are still vulnerable
+      rule.allowedVersions = `>= ${alert.fixedIn}`;
+      rule.force = {
+        ...config.vulnerabilityAlerts,
+        vulnerabilityAlert: true,
+      };
+      return rule;
+    })
+    .filter(Boolean);
+  config.packageRules = (config.packageRules || []).concat(alertPackageRules);
+  return config;
+}
diff --git a/test/workers/repository/init/__snapshots__/vulnerability.spec.js.snap b/test/workers/repository/init/__snapshots__/vulnerability.spec.js.snap
new file mode 100644
index 0000000000..eeaafc8e18
--- /dev/null
+++ b/test/workers/repository/init/__snapshots__/vulnerability.spec.js.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`workers/repository/init/vulnerability detectVulnerabilityAlerts() returns alerts 1`] = `
+Array [
+  Object {
+    "allowedVersions": ">= 1.1.0",
+    "force": Object {
+      "commitMessageSuffix": "[SECURITY]",
+      "groupName": null,
+      "schedule": Array [],
+      "vulnerabilityAlert": true,
+    },
+    "matchCurrentVersion": "< 1.1.0",
+    "packageNames": Array [
+      "some-package",
+    ],
+  },
+]
+`;
diff --git a/test/workers/repository/init/vulnerability.spec.js b/test/workers/repository/init/vulnerability.spec.js
new file mode 100644
index 0000000000..f43c4551a0
--- /dev/null
+++ b/test/workers/repository/init/vulnerability.spec.js
@@ -0,0 +1,37 @@
+let config;
+beforeEach(() => {
+  jest.resetAllMocks();
+  config = require('../../../_fixtures/config');
+});
+
+const {
+  detectVulnerabilityAlerts,
+} = require('../../../../lib/workers/repository/init/vulnerability');
+
+describe('workers/repository/init/vulnerability', () => {
+  describe('detectVulnerabilityAlerts()', () => {
+    it('returns if alerts are disabled', async () => {
+      config.vulnerabilityAlerts.enabled = false;
+      expect(await detectVulnerabilityAlerts(config)).toEqual(config);
+    });
+    it('returns if no alerts', async () => {
+      delete config.vulnerabilityAlerts.enabled;
+      platform.getVulnerabilityAlerts.mockReturnValue([]);
+      expect(await detectVulnerabilityAlerts(config)).toEqual(config);
+    });
+    it('returns alerts', async () => {
+      delete config.vulnerabilityAlerts.enabled;
+      platform.getVulnerabilityAlerts.mockReturnValue([
+        {},
+        {
+          packageName: 'some-package',
+          fixedIn: '1.1.0',
+        },
+        {},
+      ]);
+      const res = await detectVulnerabilityAlerts(config);
+      expect(res.packageRules).toMatchSnapshot();
+      expect(res.packageRules).toHaveLength(1);
+    });
+  });
+});
diff --git a/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
index 3ad256d0c8..52de9513e8 100644
--- a/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
+++ b/test/workers/repository/updates/__snapshots__/flatten.spec.js.snap
@@ -61,6 +61,11 @@ Array [
     "unpublishSafe": false,
     "updateLockFiles": true,
     "updateNotScheduled": true,
+    "vulnerabilityAlerts": Object {
+      "commitMessageSuffix": "[SECURITY]",
+      "groupName": null,
+      "schedule": Array [],
+    },
     "warnings": Array [],
     "yarnrc": null,
   },
@@ -122,6 +127,11 @@ Array [
     "unpublishSafe": false,
     "updateLockFiles": true,
     "updateNotScheduled": true,
+    "vulnerabilityAlerts": Object {
+      "commitMessageSuffix": "[SECURITY]",
+      "groupName": null,
+      "schedule": Array [],
+    },
     "warnings": Array [],
     "yarnrc": null,
   },
@@ -197,6 +207,11 @@ Array [
     "updateLockFiles": true,
     "updateNotScheduled": true,
     "updateType": "lockFileMaintenance",
+    "vulnerabilityAlerts": Object {
+      "commitMessageSuffix": "[SECURITY]",
+      "groupName": null,
+      "schedule": Array [],
+    },
     "warnings": Array [],
     "yarnrc": null,
   },
@@ -259,6 +274,11 @@ Array [
     "unpublishSafe": false,
     "updateLockFiles": true,
     "updateNotScheduled": true,
+    "vulnerabilityAlerts": Object {
+      "commitMessageSuffix": "[SECURITY]",
+      "groupName": null,
+      "schedule": Array [],
+    },
     "warnings": Array [],
     "yarnrc": null,
   },
diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md
index 986c61f481..75ba7aadda 100644
--- a/website/docs/configuration-options.md
+++ b/website/docs/configuration-options.md
@@ -702,4 +702,27 @@ When schedules are in use, it generally means "no updates". However there are ca
 
 This is default true, meaning that Renovate will perform certain "desirable" updates to _existing_ PRs even when outside of schedule. If you wish to disable all updates outside of scheduled hours then set this field to false.
 
+## vulnerabilityAlerts
+
+Use this object to customise PRs that are raised when vulnerability alerts are detected (GitHub-only). For example, to set custom labels and assignees:
+
+```json
+{
+  "vulnerabilityAlerts": {
+    "labels": ["security"],
+    "assignees": ["@rarkins"]
+  }
+}
+```
+
+To disable vulnerability alerts completely, set like this:
+
+```json
+{
+  "vulnerabilityAlerts": {
+    "enabled": false
+  }
+}
+```
+
 ## yarnrc
-- 
GitLab