From f244a4141f02334e35361820eee2f683b204201a Mon Sep 17 00:00:00 2001 From: Jesus Redondo Date: Mon, 20 Jun 2022 15:00:47 +0200 Subject: [PATCH 1/9] Add fastly key expiration check prototype --- src/checks/fastlyKeyExpiration.check.js | 39 +++++++++++++++++++++++++ src/checks/index.js | 3 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/checks/fastlyKeyExpiration.check.js diff --git a/src/checks/fastlyKeyExpiration.check.js b/src/checks/fastlyKeyExpiration.check.js new file mode 100644 index 0000000..04bc571 --- /dev/null +++ b/src/checks/fastlyKeyExpiration.check.js @@ -0,0 +1,39 @@ +const fetch = require('node-fetch'); +const moment = require('moment'); +const logger = require('@financial-times/n-logger').default; +const Check = require('./check'); +const status = require('./status'); + +const fastlyApiEndpoint = 'https://api.fastly.com/tokens/self'; + +/** + * @description Polls the current state of a Fastly key expiration date + * alert if the key expires in the next week or two + * alert if the key has a null expiry date + */ +class FastlyKeyExpirationCheck extends Check { + constructor(options) { + super(options); + this.fastlyKey = options.fastlyKey; + } + + async getFastlyKeyMetadata() { + try { + const result = await fetch(fastlyApiEndpoint, { + headers: { Fastly- Key: this.fastlyKey} + }); + const json = await result.json(); + return json; + } catch(error) { + log.error('Failed to get Fastly key metadata', error); + this.status = status.FAILED; + this.checkOutput = `Fastly keys check failed to fetch data: ${error.message}`; + } + } + + async tick() { + const metadata = await getFastlyKeyMetadata(); + } +} + +module.exports = FastlyKeyExpirationCheck; diff --git a/src/checks/index.js b/src/checks/index.js index be26061..0ed6bc8 100644 --- a/src/checks/index.js +++ b/src/checks/index.js @@ -10,5 +10,6 @@ module.exports = { graphiteThreshold: require('./graphiteThreshold.check'), graphiteWorking: require('./graphiteWorking.check'), cloudWatchAlarm: require('./cloudWatchAlarm.check'), - cloudWatchThreshold: require('./cloudWatchThreshold.check') + cloudWatchThreshold: require('./cloudWatchThreshold.check'), + fastlyKeys: require('./fastlyKeyExpiration.check') }; From 7df85357d206df462d4a027a55938ddb540189eb Mon Sep 17 00:00:00 2001 From: Jesus Redondo Date: Mon, 20 Jun 2022 15:02:52 +0200 Subject: [PATCH 2/9] Split business logic from implementation --- src/checks/fastlyKeyExpiration.check.js | 47 +++++++++++++++++++++---- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/checks/fastlyKeyExpiration.check.js b/src/checks/fastlyKeyExpiration.check.js index 04bc571..fead991 100644 --- a/src/checks/fastlyKeyExpiration.check.js +++ b/src/checks/fastlyKeyExpiration.check.js @@ -6,6 +6,29 @@ const status = require('./status'); const fastlyApiEndpoint = 'https://api.fastly.com/tokens/self'; +const states = { + PENDING: { + status: status.PENDING, + checkOutput: 'Fastly key check has not yet run' + }, + FAILED_VALIDATION: { + status: status.FAILED, + checkOutput: 'Fastly key expiration date is within 2 weeks' + }, + FAILED_DATE:{ + status: status.FAILED, + checkOutput: 'Invalid Fastly key check expiring date' + }, + ERRORED: { + status: status.ERRORED, + checkOutput: 'Fastly key check failed to fetch data' + }, + PASSED: { + status: status.PASSED, + checkOutput: 'Fastly key check has not yet run' + } +}; + /** * @description Polls the current state of a Fastly key expiration date * alert if the key expires in the next week or two @@ -17,22 +40,32 @@ class FastlyKeyExpirationCheck extends Check { this.fastlyKey = options.fastlyKey; } + setState(state) { + // To be able to assign a varialble number of parameters + Object.assign(this, state); + } + async getFastlyKeyMetadata() { try { const result = await fetch(fastlyApiEndpoint, { - headers: { Fastly- Key: this.fastlyKey} + headers: { 'Fastly-Key': this.fastlyKey } }); - const json = await result.json(); - return json; - } catch(error) { + const json = await result.json(); + return json; + } catch (error) { log.error('Failed to get Fastly key metadata', error); - this.status = status.FAILED; - this.checkOutput = `Fastly keys check failed to fetch data: ${error.message}`; + this.setState(states.ERRORED); } } + } async tick() { - const metadata = await getFastlyKeyMetadata(); + const expirationDate = await getExpirationDate(); + if (this.isValidExpirationDate(expirationDate)) { + this.setState(states.PASSED); + } else { + this.setState(states.FAILED_VALIDATION); + } } } From 400906311900c681c78c4fb2de30087a66550274 Mon Sep 17 00:00:00 2001 From: Jesus Redondo Date: Mon, 20 Jun 2022 15:03:42 +0200 Subject: [PATCH 3/9] Add getExpirationDate function --- src/checks/fastlyKeyExpiration.check.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/checks/fastlyKeyExpiration.check.js b/src/checks/fastlyKeyExpiration.check.js index fead991..bcfe73a 100644 --- a/src/checks/fastlyKeyExpiration.check.js +++ b/src/checks/fastlyKeyExpiration.check.js @@ -57,10 +57,25 @@ class FastlyKeyExpirationCheck extends Check { this.setState(states.ERRORED); } } + + parseStringDate(stringDate) { + const date = moment(stringDate, 'YYYY-MM-DDTHH:mm:ssZ'); + if (!date.isValid()) { + logger.warn(`Invalid Fastly Key expiration date ${stringDate}`); + this.setState(states.FAILED_DATE); + } + return date; + } + + async getExpirationDate() { + const metadata = await this.getFastlyKeyMetadata(); + const expirationDate = this.parseStringDate(metadata['expires_at']); + return expirationDate; + } } async tick() { - const expirationDate = await getExpirationDate(); + const expirationDate = await this.getExpirationDate(); if (this.isValidExpirationDate(expirationDate)) { this.setState(states.PASSED); } else { From 6cdcc04f6c3492804120f3ce6befa2344b87ed28 Mon Sep 17 00:00:00 2001 From: Jesus Redondo Date: Mon, 20 Jun 2022 15:04:26 +0200 Subject: [PATCH 4/9] Add isValidExpirationDate validation --- src/checks/fastlyKeyExpiration.check.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/checks/fastlyKeyExpiration.check.js b/src/checks/fastlyKeyExpiration.check.js index bcfe73a..7f85b89 100644 --- a/src/checks/fastlyKeyExpiration.check.js +++ b/src/checks/fastlyKeyExpiration.check.js @@ -72,6 +72,10 @@ class FastlyKeyExpirationCheck extends Check { const expirationDate = this.parseStringDate(metadata['expires_at']); return expirationDate; } + + isValidExpirationDate(expirationDate) { + const limitDate = moment().add(2, 'weeks'); + return expirationDate.isAfter(limitDate); } async tick() { From e953ac03d0a7b788bbd5bd07ec4667c63f7cbf15 Mon Sep 17 00:00:00 2001 From: Jesus Redondo Date: Mon, 20 Jun 2022 15:06:46 +0200 Subject: [PATCH 5/9] Fixes --- src/checks/fastlyKeyExpiration.check.js | 86 +++++++++++++++---------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/src/checks/fastlyKeyExpiration.check.js b/src/checks/fastlyKeyExpiration.check.js index 7f85b89..df5d51b 100644 --- a/src/checks/fastlyKeyExpiration.check.js +++ b/src/checks/fastlyKeyExpiration.check.js @@ -5,30 +5,6 @@ const Check = require('./check'); const status = require('./status'); const fastlyApiEndpoint = 'https://api.fastly.com/tokens/self'; - -const states = { - PENDING: { - status: status.PENDING, - checkOutput: 'Fastly key check has not yet run' - }, - FAILED_VALIDATION: { - status: status.FAILED, - checkOutput: 'Fastly key expiration date is within 2 weeks' - }, - FAILED_DATE:{ - status: status.FAILED, - checkOutput: 'Invalid Fastly key check expiring date' - }, - ERRORED: { - status: status.ERRORED, - checkOutput: 'Fastly key check failed to fetch data' - }, - PASSED: { - status: status.PASSED, - checkOutput: 'Fastly key check has not yet run' - } -}; - /** * @description Polls the current state of a Fastly key expiration date * alert if the key expires in the next week or two @@ -38,6 +14,39 @@ class FastlyKeyExpirationCheck extends Check { constructor(options) { super(options); this.fastlyKey = options.fastlyKey; + this.states = { + PENDING: { + status: status.PENDING, + checkOutput: 'Fastly key check has not yet run', + severity: this.severity + }, + FAILED_VALIDATION: { + status: status.FAILED, + checkOutput: 'Fastly key expiration date is due within 2 weeks', + severity: this.severity + }, + FAILED_URGENT_VALIDATION: { + status: status.FAILED, + checkOutput: 'Fastly key is expired', + severity: 1 + }, + FAILED_DATE: { + status: status.FAILED, + checkOutput: 'Invalid Fastly key expiring date', + severity: this.severity + }, + ERRORED: { + status: status.ERRORED, + checkOutput: 'Fastly key check failed to fetch data', + severity: this.severity + }, + PASSED: { + status: status.PASSED, + checkOutput: 'Fastly key expiration date is ok', + severity: this.severity + } + }; + this.setState(this.states.PENDING); } setState(state) { @@ -53,8 +62,9 @@ class FastlyKeyExpirationCheck extends Check { const json = await result.json(); return json; } catch (error) { - log.error('Failed to get Fastly key metadata', error); - this.setState(states.ERRORED); + logger.error('Failed to get Fastly key metadata', error.message); + this.setState(this.states.ERRORED); + throw error; } } @@ -62,7 +72,8 @@ class FastlyKeyExpirationCheck extends Check { const date = moment(stringDate, 'YYYY-MM-DDTHH:mm:ssZ'); if (!date.isValid()) { logger.warn(`Invalid Fastly Key expiration date ${stringDate}`); - this.setState(states.FAILED_DATE); + this.setState(this.states.FAILED_DATE); + throw new Error('Invalid date'); } return date; } @@ -73,17 +84,24 @@ class FastlyKeyExpirationCheck extends Check { return expirationDate; } - isValidExpirationDate(expirationDate) { + checkExpirationDate(expirationDate) { + const now = moment(); const limitDate = moment().add(2, 'weeks'); - return expirationDate.isAfter(limitDate); + switch(true){ + case expirationDate.isAfter(limitDate): return this.states.PASSED; + case expirationDate.isBefore(now): return this.states.FAILED_URGENT_VALIDATION; + default: return this.states.FAILED_VALIDATION + } } async tick() { - const expirationDate = await this.getExpirationDate(); - if (this.isValidExpirationDate(expirationDate)) { - this.setState(states.PASSED); - } else { - this.setState(states.FAILED_VALIDATION); + try { + const expirationDate = await this.getExpirationDate(); + const state = this.checkExpirationDate(expirationDate); + this.setState(state); + } catch { + // This catch is meant to end the execution of the tick function. + // An expecific state has been set for each error type. } } } From 23e18350e9ad8a0ecbf654e68c8debbd30afe145 Mon Sep 17 00:00:00 2001 From: Jesus Redondo Date: Mon, 20 Jun 2022 15:07:21 +0200 Subject: [PATCH 6/9] Add tests --- test/fastlyKeyExpiration.check.spec.js | 146 +++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 test/fastlyKeyExpiration.check.spec.js diff --git a/test/fastlyKeyExpiration.check.spec.js b/test/fastlyKeyExpiration.check.spec.js new file mode 100644 index 0000000..c5f27ad --- /dev/null +++ b/test/fastlyKeyExpiration.check.spec.js @@ -0,0 +1,146 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); +const proxyquire = require('proxyquire').noCallThru().noPreserveCache(); +const logger = require('@financial-times/n-logger').default; +const FastlyCheck = require('../src/checks/fastlyKeyExpiration.check'); + +const millisecondsFornight = 14 * 24 * 60 * 60 * 1000; +const defaultOptions = { + name: 'fastlyKeyExpiration', + severity: 2, + businessImpact: 'b', + panicGuide: 'p', + technicalSummary: 't', + fastlyKey: 'k' +}; + +describe('Fastly Key Expiration Check', () => { + beforeEach(() => { + sinon.spy(logger); + }); + afterEach(() => { + sinon.restore(); + }); + + it('returns ok=false if fastly key check has not yet run', async () => { + sinon.stub(FastlyCheck.prototype, 'tick'); + const fastlyCheck = new FastlyCheck(defaultOptions); + const result = fastlyCheck.getStatus(); + expect(result.ok).to.be.false; + // Default error message for ERRORED states in parent function Check + expect(result.checkOutput).to.be.equal( + fastlyCheck.states.PENDING.checkOutput + ); + expect(result.severity).to.be.equal(fastlyCheck.states.PENDING.severity); + }); + it('returns ok=false if key check failed to fetch data & logs errors', async () => { + // Arrange + const FastlyCheck = proxyquire('../src/checks/fastlyKeyExpiration.check', { + 'node-fetch': sinon.stub().rejects(new Error('Timeout')) + }); + const fastlyCheck = new FastlyCheck(defaultOptions); + // Force the update of the state + await fastlyCheck.tick(); + // Act + const result = fastlyCheck.getStatus(); + // Assert + expect(result.ok).to.be.false; + // Default error message for ERRORED states in parent function Check + expect(result.checkOutput).to.be.equal('Healthcheck failed to execute'); + expect(result.severity).to.be.equal(fastlyCheck.states.ERRORED.severity); + expect(logger.error.args).to.be.deep.equal([ + ['Failed to get Fastly key metadata', 'Timeout'] + ]); + }); + it('returns ok=false, severity=1 if expiration date is due now', async () => { + // Arrange + const now = new Date(); + const limitDate = new Date( + now.getTime() // now + ); + // Fix now as reference time + sinon.useFakeTimers(now); + const fastlyCheck = new FastlyCheck(defaultOptions); + const metadata = { expires_at: limitDate.toISOString() }; + sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata); + // Force the update of the state + await fastlyCheck.tick(); + // Act + const result = fastlyCheck.getStatus(); + // Assert + expect(result.ok).to.be.false; + expect(result.checkOutput).to.be.equal( + fastlyCheck.states.FAILED_URGENT_VALIDATION.checkOutput + ); + expect(result.severity).to.be.equal( + fastlyCheck.states.FAILED_URGENT_VALIDATION.severity + ); + }); + it('returns ok=false if expiration date is due within 2 weeks', async () => { + // Arrange + const now = new Date(); + const limitDate = new Date( + now.getTime() + millisecondsFornight // 2 weeks from now + ); + // Fix now as reference time + sinon.useFakeTimers(now); + const fastlyCheck = new FastlyCheck(defaultOptions); + const metadata = { expires_at: limitDate.toISOString() }; + sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata); + // Force the update of the state + await fastlyCheck.tick(); + // Act + const result = fastlyCheck.getStatus(); + // Assert + expect(result.ok).to.be.false; + expect(result.checkOutput).to.be.equal( + fastlyCheck.states.FAILED_VALIDATION.checkOutput + ); + expect(result.severity).to.be.equal( + fastlyCheck.states.FAILED_VALIDATION.severity + ); + }); + it('returns ok=true if expiration date is due after 2 weeks', async () => { + // Arrange + const now = new Date(); + const limitDate = new Date( + now.getTime() + millisecondsFornight + 1000 // 2 weeks from now + 1 second + ); + // Fix now as reference time + sinon.useFakeTimers(now); + const fastlyCheck = new FastlyCheck(defaultOptions); + const metadata = { expires_at: limitDate.toISOString() }; + sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata); + // Force the update of the state + await fastlyCheck.tick(); + // Act + const result = fastlyCheck.getStatus(); + // Assert + expect(result.ok).to.be.true; + expect(result.checkOutput).to.be.equal( + fastlyCheck.states.PASSED.checkOutput + ); + expect(result.severity).to.be.equal(fastlyCheck.states.PASSED.severity); + }); + it('returns ok=false if expiration date is not valid & logs warning', async () => { + // Arrange + const fastlyCheck = new FastlyCheck(defaultOptions); + const metadata = { expires_at: 'aaaa' }; + sinon.stub(fastlyCheck, 'getFastlyKeyMetadata').resolves(metadata); + // Force the update of the state + await fastlyCheck.tick(); + // Act + const result = fastlyCheck.getStatus(); + // Assert + expect(result.ok).to.be.false; + expect(result.checkOutput).to.be.equal( + fastlyCheck.states.FAILED_DATE.checkOutput + ); + expect(result.severity).to.be.equal( + fastlyCheck.states.FAILED_DATE.severity + ); + expect(logger.warn.args).to.be.deep.equal([ + ['Invalid Fastly Key expiration date aaaa'] + ]); + }); +}); From 8b82627172d272e6547911913a549db5e497c6e9 Mon Sep 17 00:00:00 2001 From: Jesus Redondo Date: Mon, 20 Jun 2022 16:59:59 +0200 Subject: [PATCH 7/9] Fix typo millisecondsFortnight Co-authored-by: Ivo Murrell --- test/fastlyKeyExpiration.check.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fastlyKeyExpiration.check.spec.js b/test/fastlyKeyExpiration.check.spec.js index c5f27ad..7d03465 100644 --- a/test/fastlyKeyExpiration.check.spec.js +++ b/test/fastlyKeyExpiration.check.spec.js @@ -4,7 +4,7 @@ const proxyquire = require('proxyquire').noCallThru().noPreserveCache(); const logger = require('@financial-times/n-logger').default; const FastlyCheck = require('../src/checks/fastlyKeyExpiration.check'); -const millisecondsFornight = 14 * 24 * 60 * 60 * 1000; +const millisecondsFortnight = 14 * 24 * 60 * 60 * 1000; const defaultOptions = { name: 'fastlyKeyExpiration', severity: 2, From 8c28c8beab27b3001a23c67246c342b145a5c073 Mon Sep 17 00:00:00 2001 From: Jesus Redondo Date: Mon, 20 Jun 2022 17:10:48 +0200 Subject: [PATCH 8/9] Freeze FastlyKeyExpirationCheck states & test for it --- src/checks/fastlyKeyExpiration.check.js | 4 +-- test/fastlyKeyExpiration.check.spec.js | 39 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/checks/fastlyKeyExpiration.check.js b/src/checks/fastlyKeyExpiration.check.js index df5d51b..01ffdd6 100644 --- a/src/checks/fastlyKeyExpiration.check.js +++ b/src/checks/fastlyKeyExpiration.check.js @@ -14,7 +14,7 @@ class FastlyKeyExpirationCheck extends Check { constructor(options) { super(options); this.fastlyKey = options.fastlyKey; - this.states = { + this.states = Object.freeze({ PENDING: { status: status.PENDING, checkOutput: 'Fastly key check has not yet run', @@ -45,7 +45,7 @@ class FastlyKeyExpirationCheck extends Check { checkOutput: 'Fastly key expiration date is ok', severity: this.severity } - }; + }); this.setState(this.states.PENDING); } diff --git a/test/fastlyKeyExpiration.check.spec.js b/test/fastlyKeyExpiration.check.spec.js index c5f27ad..d9acc10 100644 --- a/test/fastlyKeyExpiration.check.spec.js +++ b/test/fastlyKeyExpiration.check.spec.js @@ -3,6 +3,7 @@ const { expect } = require('chai'); const proxyquire = require('proxyquire').noCallThru().noPreserveCache(); const logger = require('@financial-times/n-logger').default; const FastlyCheck = require('../src/checks/fastlyKeyExpiration.check'); +const status = require('../src/checks/status'); const millisecondsFornight = 14 * 24 * 60 * 60 * 1000; const defaultOptions = { @@ -21,6 +22,44 @@ describe('Fastly Key Expiration Check', () => { afterEach(() => { sinon.restore(); }); + it('FastlyKeyExpirationCheck instance set & freeze states properly ', () => { + const fastlyKeyExpirationCheck = new FastlyCheck(defaultOptions); + const initialStates = { + PENDING: { + status: status.PENDING, + checkOutput: 'Fastly key check has not yet run', + severity: 2 + }, + FAILED_VALIDATION: { + status: status.FAILED, + checkOutput: 'Fastly key expiration date is due within 2 weeks', + severity: 2 + }, + FAILED_URGENT_VALIDATION: { + status: status.FAILED, + checkOutput: 'Fastly key is expired', + severity: 1 + }, + FAILED_DATE: { + status: status.FAILED, + checkOutput: 'Invalid Fastly key expiring date', + severity: 2 + }, + ERRORED: { + status: status.ERRORED, + checkOutput: 'Fastly key check failed to fetch data', + severity: 2 + }, + PASSED: { + status: status.PASSED, + checkOutput: 'Fastly key expiration date is ok', + severity: 2 + } + }; + expect(fastlyKeyExpirationCheck.states).to.be.deep.equal(initialStates); + fastlyKeyExpirationCheck.severity = 1; + expect(fastlyKeyExpirationCheck.states).to.be.deep.equal(initialStates); + }); it('returns ok=false if fastly key check has not yet run', async () => { sinon.stub(FastlyCheck.prototype, 'tick'); From b85aa53225ea59ca43190c311e1c9dd900464c1e Mon Sep 17 00:00:00 2001 From: Jesus Redondo Date: Mon, 20 Jun 2022 17:16:07 +0200 Subject: [PATCH 9/9] Fix typo millisecondsFortnight --- test/fastlyKeyExpiration.check.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fastlyKeyExpiration.check.spec.js b/test/fastlyKeyExpiration.check.spec.js index b11a357..2c8a8b7 100644 --- a/test/fastlyKeyExpiration.check.spec.js +++ b/test/fastlyKeyExpiration.check.spec.js @@ -119,7 +119,7 @@ describe('Fastly Key Expiration Check', () => { // Arrange const now = new Date(); const limitDate = new Date( - now.getTime() + millisecondsFornight // 2 weeks from now + now.getTime() + millisecondsFortnight // 2 weeks from now ); // Fix now as reference time sinon.useFakeTimers(now); @@ -143,7 +143,7 @@ describe('Fastly Key Expiration Check', () => { // Arrange const now = new Date(); const limitDate = new Date( - now.getTime() + millisecondsFornight + 1000 // 2 weeks from now + 1 second + now.getTime() + millisecondsFortnight + 1000 // 2 weeks from now + 1 second ); // Fix now as reference time sinon.useFakeTimers(now);