Skip to content
This repository was archived by the owner on Jun 9, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .hawkeyerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"all": true,
"modules": ["files-secrets"],
"failOn": "low",
"showCode": false,
"moduleConfig": {
"files-secrets": {
"files": [
"patterns.json"
]
}
}
}
17 changes: 8 additions & 9 deletions bin/hawkeye-modules
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

'use strict'

const logger = require('../lib/logger')
const modules = require('../lib/modules')
require('colors')

logger.log('Module Status'.bold)
modules().forEach(m => {
logger.log(`${m.enabled ? 'Enabled: '.green : 'Disabled: '.red} ${m.key.bold}`)
logger.log(' ' + m.description)
})
const program = require('commander')
program
.command('config <module>', 'Module configuration documentation')
.command('ls', 'Lists the currently install modules', {isDefault: true})
.parse(process.argv)



43 changes: 43 additions & 0 deletions bin/hawkeye-modules-config
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env node

'use strict'

const modules = require('../lib/modules')
require('colors')
const program = require('commander')

program
.arguments('<module>')
.action(printModuleConfigurationDocumentation);

program.parse(process.argv);

function printModuleConfigurationDocumentation(moduleKey) {
console.log(`${moduleKey} module configuration properties:`.bold)
console.log('')

let module = modules().filter(m => m.key === moduleKey)[0];
const schema = module.configSchema;
for (const [k, v] of Object.entries(schema.properties)) {
let type = v.type;
if (type === 'array') {
type = v.items.type + '[]'
}
console.log(` ${k.bold} (${type}): ${v.title}`)
}

const exampleConfig = {
moduleConfig:{
}
}

console.log('')
console.log('.hawkeyerc examples:'.bold)
schema.examples.forEach((e) => {
exampleConfig.moduleConfig[module.key] = e;
let example = JSON.stringify(exampleConfig, undefined, 2).padStart(4);
console.log(example)
console.log('')
})
}

13 changes: 13 additions & 0 deletions bin/hawkeye-modules-ls
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env node

'use strict'

const logger = require('../lib/logger')
const modules = require('../lib/modules')
require('colors')

logger.log('Module Status'.bold)
modules().forEach(m => {
logger.log(`${m.enabled ? 'Enabled: '.green : 'Disabled: '.red} ${m.key.bold}`)
logger.log(' ' + m.description)
})
82 changes: 80 additions & 2 deletions lib/__tests__/modules-unit.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,92 @@
'use strict'

/* eslint-disable no-unused-expressions */

const Ajv = require('ajv')

const modules = require('../modules')

describe('Modules', () => {
modules().forEach(module => {
describe(module.key, () => {
it('should have the module signature', () => {
it('should have required properties', () => {
let expectMethods = ['key', 'description', 'handles', 'run', 'enabled'].sort()
expect(Object.keys(module).sort()).to.deep.equal(expectMethods)
expect(module).to.include.all.keys(expectMethods)
})
})
})
})

describe('Configurable Modules', () => {
modules()
.filter(m => m.configSchema)
.forEach(module => {
describe(`${module.key} config schema`, () => {
it('should be a valid JSON Schema', () => {
const ajv = new Ajv({
strictDefaults: true,
strictKeywords: true
})

expect(ajv.validateSchema(module.configSchema)).to.be.true
})

it('should expect an object', () => {
expect(module.configSchema.type).to.be.equal('object')
})

it('should have at least one example', () => {
expect(module.configSchema.examples).to.have.lengthOf.at.least(1)
})

it('should have valid examples', () => {
const ajv = new Ajv({
strictDefaults: true,
strictKeywords: true,
removeAdditional: true
})

if (!module.configSchema.hasOwnProperty('additionalProperties')) {
module.configSchema.additionalProperties = false
}

module.configSchema.examples.forEach((e, i) => {
const validate = ajv.compile(module.configSchema)
expect(validate(e), `Example #${i}`).to.be.true
})
})

it('should have some title for root level properties', () => {
for (const [property, definition] of Object.entries(module.configSchema.properties)) {
expect(definition.title, property).to.not.be.empty
}
})

it('should have some type defined for root level properties', () => {
for (const [property, definition] of Object.entries(module.configSchema.properties)) {
expect(definition.type, property).to.not.be.empty

if (definition.type === 'array') {
expect(definition.items.type, `${property} items`).to.not.be.empty
}
}
})

it('should accept empty object as valid config', () => {
const ajv = new Ajv({
strictDefaults: true,
strictKeywords: true,
removeAdditional: true,
useDefaults: true
})

if (!module.configSchema.hasOwnProperty('additionalProperties')) {
module.configSchema.additionalProperties = false
}

const validate = ajv.compile(module.configSchema)
expect(validate({})).to.be.true
})
})
})
})
26 changes: 26 additions & 0 deletions lib/modules/files-secrets/config-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Schema of Hawkeye module files-secrets",
"properties": {
"files": {
"type": "array",
"title": "List of files containing patterns of suspicious filenames",
"default": [],
"items": {
"type": "string",
"title": "Path to file",
"examples": [
"path/to/patterns.json"
]
}
}
},
"examples": [
{
"files": [
"path/to/patterns.json"
]
}
]
}
Copy link
Contributor Author

@bekh6ex bekh6ex Jul 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To generate reasonable documentation (help) we can enforce by tests things like:

  • each config should have an example (that is up to date and passes schema test)
  • each property has a title and a type
  • ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we can try to use this definition for cli arguments. Not sure how hard it will be if we want to make it good

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... or limit cli arguments to what we have now and allow to pass .hawkeyerc as an argument or even read it from stdIn

7 changes: 5 additions & 2 deletions lib/modules/files-secrets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ module.exports = {
key,
description: 'Scans for suspicious filenames that are likely to contain secrets',
enabled: true,
handles: () => true,
run: fm => {
handles: (fm, config) => true,
configSchema: require('./config-schema.json'),
run: (fm, config) => {
// TODO Access `config.files` here to read custom patterns' definitions

const results = new ModuleResults(key)

const checkers = items.map(item => {
Expand Down
9 changes: 8 additions & 1 deletion lib/rc.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = class RC {
this.all = false
this.staged = false
this.showCode = false
this.moduleConfig = {}
this.writers = [consoleWriter]
}

Expand Down Expand Up @@ -95,7 +96,8 @@ module.exports = class RC {
http: this.withHttp.bind(this),
json: this.withJson.bind(this),
failOn: this.withFailOn.bind(this),
showCode: this.withShowCode.bind(this)
showCode: this.withShowCode.bind(this),
moduleConfig: this.withModuleConfig.bind(this)
}
Object.keys(hawkeyerc).forEach(key => {
const handler = handlers[key]
Expand Down Expand Up @@ -149,4 +151,9 @@ module.exports = class RC {
this.writers.push({ ...httpWriter, opts: { url } })
return this
}

withModuleConfig (config) {
this.moduleConfig = config || {}
return this
}
}
36 changes: 34 additions & 2 deletions lib/scan.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const modules = require('./modules')
const logger = require('./logger')
const _ = require('lodash')
require('colors')
const Ajv = require('ajv')

module.exports = async (rc = {}) => {
logger.log('Target for scan:', rc.target)
Expand All @@ -26,9 +27,14 @@ module.exports = async (rc = {}) => {

const activeModules = []
const inactiveModules = []
knownModules
.filter(canBeConfigured)
.filter(m => rc.moduleConfig.hasOwnProperty(m.key))
.forEach(m => validateAndNormalizeModuleConfig(m, rc.moduleConfig[m.key]))

for (const module of knownModules) {
logger.log(`Checking ${module.key} for applicability`)
const isActive = await module.handles(fm)
const isActive = await module.handles(fm, rc.moduleConfig[module.key])
;(isActive ? activeModules : inactiveModules).push(module)
}
inactiveModules.forEach(module => logger.log('Skipping module'.bold, module.key))
Expand All @@ -41,7 +47,7 @@ module.exports = async (rc = {}) => {
.reduce((prom, { key, run }) => prom.then(async allRes => {
logger.log('Running module'.bold, key)
try {
const res = await run(fm)
const res = await run(fm, rc.moduleConfig[module.key])
return allRes.concat(res)
} catch (e) {
logger.error(key, 'returned an error!', e.message)
Expand Down Expand Up @@ -73,3 +79,29 @@ module.exports = async (rc = {}) => {

return results.length ? 1 : 0
}

function canBeConfigured (module) {
return !!module.configSchema
}

/**
* Not only validates the config, but also removes fields that are not defined in schema and
* puts default value in if property is undefined
*/
function validateAndNormalizeModuleConfig (module, config) {
const ajv = new Ajv({
strictDefaults: true,
strictKeywords: true,
removeAdditional: true,
useDefaults: true
})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeAdditional - remove additional (nondocumented) properties from config object. Might be useful to force module developers documenting all possible config values.
useDefaults - if property is not defined and has default value will modify config object by injecting the property with default value. Just handy.

if (!module.configSchema.hasOwnProperty('additionalProperties')) {
module.configSchema.additionalProperties = false
}

const validate = ajv.compile(module.configSchema)
const valid = validate(config)
if (!valid) {
throw new Error(`Config for module '${module.key}' is invalid:\n` + validate.errors.map(JSON.stringify).join('\n'))
}
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"watch": "nodemon --watch . --exec 'npm test'",
"test": "npx npm-run-all test:lint test:unit",
"test:lint": "npx standard",
"test:lint:fix": "npx standard --fix",
"test:unit": "NODE_ENV=testing npx mocha 'lib/**/*-unit.js' -R 'spec' testutils.js"
},
"bin": {
Expand All @@ -56,7 +57,8 @@
"semver": "^6.1.1",
"superagent": "^5.0.5",
"tmp": "^0.1.0",
"xml2js": "^0.4.19"
"xml2js": "^0.4.19",
"ajv": "^6.10.0"
},
"devDependencies": {
"chai": "^4.2.0",
Expand Down