Skip to content
This repository was archived by the owner on Jun 9, 2022. It is now read-only.

Commit aec9ef0

Browse files
committed
Refactor writers
1 parent 3ac84ef commit aec9ef0

18 files changed

+457
-283
lines changed

bin/hawkeye-scan

+5-24
Original file line numberDiff line numberDiff line change
@@ -36,28 +36,9 @@ if (!rc.target) {
3636
rc.withTarget(process.env.PWD)
3737
}
3838

39-
scan(rc).then(results => {
40-
let total = 0
41-
let exitCode = 0
42-
results.forEach(moduleResult => {
43-
Object.keys(moduleResult.results).forEach(key => {
44-
const levelResults = moduleResult.results[key].length
45-
const threshold = { low: 1, medium: 2, high: 4, critical: 8 }
46-
if (levelResults > 0 && threshold[key] >= threshold[rc.failOn]) { exitCode = 1 }
47-
total = total + levelResults
48-
})
39+
scan(rc)
40+
.then(code => { process.exit(code) })
41+
.catch(e => {
42+
logger.error('Unexpected error occurred!', e.message)
43+
process.exit(42)
4944
})
50-
logger.log('scan complete, ' + total + ' issues found')
51-
rc.writers.forEach(writer => {
52-
logger.log('Doing writer:', writer.key)
53-
const state = exitCode === 1 ? 'fail' : 'pass'
54-
writer.write(results, { state }, err => err && logger.error(err.message))
55-
})
56-
logger.log('Scan complete')
57-
if (total > 0) {
58-
process.exit(exitCode)
59-
}
60-
}).catch(e => {
61-
console.error(e)
62-
process.exit(1)
63-
})

lib/__tests__/rc-unit.js

+32-20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
'use strict'
2+
3+
/* eslint-disable no-unused-expressions */
4+
25
const Rc = require('../rc')
36
const path = require('path')
47

@@ -35,38 +38,47 @@ describe('RC', () => {
3538
})
3639

3740
describe('withSumo', () => {
38-
it('should let me add a sumo writer ', () => {
39-
rc.withSumo('http://url.com')
40-
expect(rc.sumo).to.equal('http://url.com')
41+
it('should configure writer from hawkeyerc', () => {
42+
expect(rc.writers.filter(w => w.key === 'writer-sumo').length === 1).to.be.true
4143
})
42-
it('sumo writer should not allow invalid urls', () => {
43-
expect(() => {
44-
rc.withSumo('bad-url')
45-
}).to.throw()
44+
it('should add writer ', () => {
45+
noRc.withSumo('http://url.com')
46+
const [writer] = rc.writers.filter(w => w.key === 'writer-sumo')
47+
expect(writer.key).to.equal('writer-sumo')
48+
expect(writer.opts).to.deep.equal({ url: 'http://url.com' })
49+
})
50+
it('should not allow invalid urls', () => {
51+
expect(() => { noRc.withSumo('bad-url') }).to.throw()
4652
})
4753
})
4854

4955
describe('withJson', () => {
50-
it('should let me add a json writer ', () => {
51-
rc.withJson('path')
52-
expect(rc.json).to.equal('path')
56+
it('should configure from hawkeyerc', () => {
57+
expect(rc.writers.filter(w => w.key === 'writer-json').length === 1).to.be.true
58+
})
59+
it('should add writer ', () => {
60+
noRc.withJson('path')
61+
const [writer] = rc.writers.filter(w => w.key === 'writer-json')
62+
expect(writer.key).to.equal('writer-json')
63+
expect(writer.opts).to.deep.equal({ file: 'path' })
5364
})
5465
it('should reject bad paths', () => {
55-
expect(() => {
56-
rc.withJson('*!&@*$^path')
57-
}).to.throw()
66+
expect(() => { rc.withJson('*!&@*$^path') }).to.throw()
5867
})
5968
})
6069

6170
describe('withHttp', () => {
62-
it('should let me add a http writer ', () => {
63-
rc.withHttp('http://url.com')
64-
expect(rc.http).to.equal('http://url.com')
71+
it('should configure writer from hawkeyerc', () => {
72+
expect(rc.writers.filter(w => w.key === 'writer-http').length === 1).to.be.true
6573
})
66-
it('http writer should not allow invalid urls', () => {
67-
expect(() => {
68-
rc.withHttp('bad-url')
69-
}).to.throw()
74+
it('should add writer ', () => {
75+
noRc.withHttp('http://url.com')
76+
const [writer] = rc.writers.filter(w => w.key === 'writer-http')
77+
expect(writer.key).to.equal('writer-http')
78+
expect(writer.opts).to.deep.equal({ url: 'http://url.com' })
79+
})
80+
it('should not allow invalid urls', () => {
81+
expect(() => { noRc.withHttp('bad-url') }).to.throw()
7082
})
7183
})
7284

lib/rc.js

+17-23
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ const fs = require('fs')
33
const path = require('path')
44
const util = require('./util')
55
const isValidPath = require('is-valid-path')
6-
const ConsoleWriter = require('./writers/console')
7-
const JsonWriter = require('./writers/json')
8-
const SumoWriter = require('./writers/sumologic')
9-
const HttpWriter = require('./writers/http')
6+
const consoleWriter = require('./writers/console')
7+
const jsonWriter = require('./writers/json')
8+
const sumoWriter = require('./writers/sumo')
9+
const httpWriter = require('./writers/http')
1010
const logger = require('./logger')
1111

1212
module.exports = class RC {
@@ -16,7 +16,7 @@ module.exports = class RC {
1616
this.modules = ['all']
1717
this.all = false
1818
this.staged = false
19-
this.writers = [new ConsoleWriter()]
19+
this.writers = [consoleWriter]
2020
}
2121

2222
withStaged () {
@@ -122,33 +122,27 @@ module.exports = class RC {
122122
return this
123123
}
124124

125-
withJson (path) {
126-
if (!isValidPath(path)) {
127-
throw new Error(path + ' is not a valid path')
125+
withJson (file) {
126+
if (!isValidPath(file)) {
127+
throw new Error(file + ' is not a valid path')
128128
}
129-
this.json = path
130-
this.writers.push(new JsonWriter({ path: this.json }))
129+
this.writers.push({ ...jsonWriter, opts: { file } })
131130
return this
132131
}
133132

134133
withSumo (url) {
135-
isValidUrl(url)
136-
this.sumo = url
137-
this.writers.push(new SumoWriter({ url }))
134+
if (!/^(https?):\/\/.*$/.test(url)) {
135+
throw new Error('Invalid URL: ' + url)
136+
}
137+
this.writers.push({ ...sumoWriter, opts: { url } })
138138
return this
139139
}
140140

141141
withHttp (url) {
142-
isValidUrl(url)
143-
this.http = url
144-
this.writers.push(new HttpWriter({ url: this.http }))
142+
if (!/^(https?):\/\/.*$/.test(url)) {
143+
throw new Error('Invalid URL: ' + url)
144+
}
145+
this.writers.push({ ...httpWriter, opts: { url } })
145146
return this
146147
}
147148
}
148-
149-
const isValidUrl = (userInput) => {
150-
var urlregex = /^(https?):\/\/.*$/
151-
if (!urlregex.test(userInput)) {
152-
throw new Error('Invalid URL: ' + userInput)
153-
}
154-
}

lib/scan.js

+32-32
Original file line numberDiff line numberDiff line change
@@ -37,38 +37,38 @@ module.exports = async (rc = {}) => {
3737
throw new Error('We found no modules that would run on the target folder')
3838
}
3939

40-
const results = []
41-
for (const module of activeModules) {
42-
logger.log('Running module'.bold, module.key)
43-
try {
44-
const result = await module.run(fm)
45-
results.push(result)
46-
} catch (e) {
47-
logger.error(module.key, 'returned an error!', e.message)
48-
}
40+
let results = await activeModules
41+
.reduce((prom, { key, run }) => prom.then(async allRes => {
42+
logger.log('Running module'.bold, key)
43+
try {
44+
const res = await run(fm)
45+
return allRes.concat(res)
46+
} catch (e) {
47+
logger.error(key, 'returned an error!', e.message)
48+
return allRes
49+
}
50+
}), Promise.resolve([]))
51+
52+
const threshold = { low: 1, medium: 2, high: 4, critical: 8 }
53+
54+
results = results
55+
.map(({ key, data }) => ({
56+
critical: data.critical.map(res => Object.assign({ module: key, level: 'critical' }, res, {})),
57+
high: data.high.map(res => Object.assign({ module: key, level: 'high' }, res, {})),
58+
medium: data.medium.map(res => Object.assign({ module: key, level: 'medium' }, res, {})),
59+
low: data.low.map(res => Object.assign({ module: key, level: 'low' }, res, {}))
60+
}))
61+
.map(module => Object.keys(module).reduce((acc, lvl) => acc.concat(module[lvl]), []))
62+
.reduce((flatmap, results) => flatmap.concat(results), [])
63+
.map(res => _.omit(res, ['code']))
64+
.filter(res => threshold[res.level] >= threshold[rc.failOn])
65+
66+
logger.log(`Scan complete, ${results.length} issues found`)
67+
68+
for (const { key, write, opts } of rc.writers) {
69+
logger.log(`Writing to: ${key}`)
70+
await write(results, opts)
4971
}
5072

51-
return results.map(result => {
52-
let tmp = result.results
53-
switch (rc.threshold) {
54-
case 'critical':
55-
delete tmp.high
56-
delete tmp.medium
57-
delete tmp.low
58-
break
59-
case 'high':
60-
delete tmp.medium
61-
delete tmp.low
62-
break
63-
case 'medium':
64-
delete tmp.low
65-
break
66-
default:
67-
break
68-
}
69-
return {
70-
module: result.key,
71-
results: tmp
72-
}
73-
})
73+
return results.length ? 1 : 0
7474
}

lib/writers/__tests__/console-unit.js

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict'
2+
3+
/* eslint-disable no-unused-expressions */
4+
5+
const { write } = require('../console')
6+
7+
describe('Writer', () => {
8+
it('should write to console', async () => {
9+
sinon.stub(console, 'table')
10+
const payload = [{
11+
module: 'files-ccnumber',
12+
level: 'critical',
13+
code: 'files-secrets-47',
14+
offender: 'testfile1.yml',
15+
description: 'Contains word: password',
16+
mitigation: 'Check contents of the file'
17+
}, {
18+
module: 'files-ccnumber',
19+
level: 'critical',
20+
code: 'files-secrets-47',
21+
offender: 'testfile2.yml',
22+
description: 'Contains word: password',
23+
mitigation: 'Check contents of the file'
24+
}, {
25+
module: 'files-contents',
26+
level: 'critical',
27+
code: 'files-contents-2',
28+
offender: 'testfile3.yml',
29+
description: 'Private key in file',
30+
mitigation: 'Check line number: 3'
31+
}]
32+
33+
await write(payload)
34+
35+
expect(console.table).to.have.been.calledOnce
36+
expect(console.table).to.have.been.calledWith(payload)
37+
})
38+
})

lib/writers/__tests__/http-unit.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict'
2+
3+
const nock = require('nock')
4+
const { write } = require('../http')
5+
6+
const host = 'http://host.foobar'
7+
const path = '/collector'
8+
const opts = { url: host + path }
9+
10+
describe('Writer', () => {
11+
it('should send to collector', async () => {
12+
const payload1 = {
13+
module: 'files-ccnumber',
14+
level: 'critical',
15+
code: 'files-secrets-47',
16+
offender: 'testfile1.yml',
17+
description: 'Contains word: password',
18+
mitigation: 'Check contents of the file'
19+
}
20+
const payload2 = {
21+
module: 'files-ccnumber',
22+
level: 'critical',
23+
code: 'files-secrets-47',
24+
offender: 'testfile2.yml',
25+
description: 'Contains word: password',
26+
mitigation: 'Check contents of the file'
27+
}
28+
const payload3 = {
29+
module: 'files-contents',
30+
level: 'critical',
31+
code: 'files-contents-2',
32+
offender: 'testfile3.yml',
33+
description: 'Private key in file',
34+
mitigation: 'Check line number: 3'
35+
}
36+
37+
nock(host, { reqheaders: { 'User-Agent': 'hawkeye' } })
38+
.post(path, payload1)
39+
.reply(200)
40+
.post(path, payload2)
41+
.reply(200)
42+
.post(path, payload3)
43+
.reply(200)
44+
45+
await write([payload1, payload2, payload3], opts)
46+
})
47+
})

lib/writers/__tests__/json-unit.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict'
2+
3+
const { write } = require('../json')
4+
const path = require('path')
5+
const { readFileSync, unlinkSync } = require('fs')
6+
7+
const metadata = {
8+
file: path.join(__dirname, 'testfile.json')
9+
}
10+
11+
describe('JSON Writer', () => {
12+
it('should write JSON to a file', async () => {
13+
const payload = { 'key': 'value' }
14+
const expected = JSON.stringify(payload)
15+
16+
await write(payload, metadata)
17+
18+
expect(readFileSync(metadata.file).toString()).to.equal(expected)
19+
unlinkSync(metadata.file)
20+
})
21+
})

0 commit comments

Comments
 (0)