Skip to content

Commit

Permalink
feat: Allow comments to be inserted into the security.txt file
Browse files Browse the repository at this point in the history
We provide a way for developers to insert comments within a security.txt file (i.e, lines starting with a #) by allowing special objects and additional special keys. We also document this, as well as other features we've added in the past.

#33
  • Loading branch information
joker314 authored Nov 5, 2018
1 parent 0f0ee53 commit aa23c88
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 9 deletions.
76 changes: 75 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,88 @@ and use it as a middleware for an express app.
const securityTxt = require('express-security-txt')

const options = {
contact: '[email protected]',
contact: 'mailto:[email protected]',
encryption: 'https://www.mykey.com/pgp-key.txt',
acknowledgement: 'thank you'
}

app.use(securityTxt.setup(options))
```
### Chaining

Where allowed, you can provide multiple values for a single directive by passing an array.

```js
const securityTxt = require('express-security-txt')

const options = {
contact: [
'https://firstMethodOfContact.example.com',
'https://secondMethodOfContact.example.com'
]
}

app.use(securityTxt.setup(options))
```

### Comments

To add a comment at the beggining or end of the security.txt file, one may use the keys `_prefixComment` and `_postfixComment` respectively. If one wishes to place a comment immediately before a field, one may use an object which specifies the value of the field and the comment which must come before it.

```js
const securityTxt = require('express-security-txt')

const options = {
_prefixComment: 'This comment goes at the very beggining of the file',
contact: {
comment: 'This comment goes directly before the Contact: directive',
value: 'mailto:[email protected]'
},
encryption: [
'https://example.com/encryption',
{
comment: 'Comments can appear in the middle of an array of values',
value: 'https://example.com/alternativeEncryption'
}
],
_postfixComment: 'This comment goes at the very end of the file'
}

app.use(securityTxt.setup(options))
```

Would generate the file

```txt
# This comment goes at the very beggining of the file
# This comment goes directly before the Contact: directive
Contact: mailto:[email protected]
Encryption: https://example.com/encryption
# Comments can appear in the middle of an array of values
Encryption: https://example.com/alternativeEncryption
# This comment goes at the very end of the file
```

If your comment spans multiple lines, you can use `\n` to split it. express-security-txt will automatically insert the relevant `#` symbols. Alternatively, one can use an array of lines instead of a string.

For example:

```js
const options = {
_prefixComment: ['this is a', 'comment\nwhich', 'spans many lines'],
contact: 'mailto:[email protected]'
}
```

Would generate

```txt
# this is a
# comment
# which
# spans many lines
Contact: mailto:[email protected]
```
## Tests

Project tests:
Expand Down
40 changes: 40 additions & 0 deletions __tests__/formatPolicy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,43 @@ test('camelCasing works for different types of directives', () => {
expect(securityTxt.camelCase('Abc-Def')).toBe('abcDef')
expect(securityTxt.camelCase('Abc-Def-Ghi')).toBe('abcDefGhi')
})

test('formats successfully with comments', () => {
const options = {
contact: {
comment: 'b',
value: 'tel:+123'
},
encryption: [
{
value: 'https://a.example.com'
},
{
value: 'https://b.example.com',
comment: ['c', 'h', 'i\nj']
},
'https://c.example.com'
],
_prefixComment: ['a', 'z', 'x\ny'],
_postfixComment: 'd'
}

const res = securityTxt.formatSecurityPolicy(options)

expect(res).toBe(
'# a\n' +
'# z\n' +
'# x\n' +
'# y\n' +
'# b\n' +
'Contact: tel:+123\n' +
'Encryption: https://a.example.com\n' +
'# c\n' +
'# h\n' +
'# i\n' +
'# j\n' +
'Encryption: https://b.example.com\n' +
'Encryption: https://c.example.com\n' +
'# d\n'
)
})
62 changes: 62 additions & 0 deletions __tests__/validatePolicy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,65 @@ test('validate fails when providing arrays for signature/permission', () => {

expect(() => securityTxt.validatePolicyFields(options)).toThrow()
})

test('validate successfully when using prefix/postfix comments', () => {
const options = {
_prefixComment: ['This is a\nprefix', 'comment'],
_postfixComment: 'This is a \npostfix comment',
contact: 'mailto:[email protected]'
}

expect(() => securityTxt.validatePolicyFields(options)).not.toThrow()
})

test('validate successfully when using objects for comments', () => {
const options = {
contact: [
{
comment: ['...', '...'],
value: 'mailto:[email protected]'
},
{
value: 'tel:+123'
}
],
encryption: {
comment: '...',
value: 'https://encryption.example.com'
}
}

expect(() => securityTxt.validatePolicyFields(options)).not.toThrow()
})

test('validate fails when not providing a value in comment object', () => {
const singleObject = {
contact: {
comment: ''
}
}

const arrayOfObjects = {
contact: [
{
comment: '...',
value: 'tel:+123'
},
{
comment: '...'
}
]
}

expect(() => securityTxt.validatePolicyFields(singleObject)).toThrow()
expect(() => securityTxt.validatePolicyFields(arrayOfObjects)).toThrow()
})

test('validate fails when using a [{value: [...]}] nested array', () => {
const options = {
contact: [{ value: ['test'] }],
encryption: [{ value: ['test'] }]
}

expect(() => securityTxt.validatePolicyFields(options)).toThrow()
})
90 changes: 82 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ class middleware {
// Before applying formatting let's validate the options
this.validatePolicyFields(options)

const asComment = comment => {
const flatten = (a, b) => a.concat(b)

if (!Array.isArray(comment)) {
comment = [ comment ]
}

return comment
.map(n => n.split`\n`)
.reduce(flatten, [])
.map(n => `# ${n}\n`)
.join``
}

let policySettingText = ''

const tmpPolicyArray = []
Expand All @@ -46,15 +60,31 @@ class middleware {

let value = options[key] // eslint-disable-line security/detect-object-injection

if (typeof value !== 'object') {
if (!Array.isArray(value)) {
value = [ value ]
}

value.forEach(valueOption => {
if (valueOption.hasOwnProperty('value')) {
if (valueOption.hasOwnProperty('comment')) {
tmpPolicyArray.push(asComment(valueOption.comment))
}

valueOption = valueOption.value
}

tmpPolicyArray.push(`${directive}: ${valueOption}\n`)
})
}

if (typeof options._prefixComment !== 'undefined') {
tmpPolicyArray.unshift(asComment(options._prefixComment))
}

if (typeof options._postfixComment !== 'undefined') {
tmpPolicyArray.push(asComment(options._postfixComment))
}

policySettingText = tmpPolicyArray.join('')
return policySettingText
}
Expand All @@ -68,15 +98,59 @@ class middleware {
static validatePolicyFields (options) {
const array = Joi.array().single()
const string = Joi.string()
const comment = array.items(string)

/**
* A function to create a custom schema for a security.txt
* field value.
*
* @param {object} [options={}] - requirements of this schema
* @param {boolean} [options.canBeArray=true] - can singleValue appear in an array
* @param {object} [singleValue=Joi.string()] - a Joi schema to validate a single entry (e.g. of an array)
* @param {boolean} [required=false] - whether this schema must be present
*/
const fieldValue = ({ canBeArray = true, singleValue = string, required = false } = {}) => {
let schema = Joi.alternatives()

/**
* A function which returns a schema for a comment object (of the form { comment: ..., value: ... })
*
* @param {boolean} [arrayAllowed=canBeArray] - Whether values can be arrays of values
* @return {object} - a Joi schema
*/
function commentSchema (arrayAllowed = canBeArray) {
return Joi.object().keys({
comment: comment,
value: (arrayAllowed ? array.items(singleValue) : singleValue).required()
})
}

schema = schema.try(singleValue)
schema = schema.try(commentSchema())

if (canBeArray) {
schema = schema.try(array.items(
Joi.alternatives().try(singleValue).try(commentSchema(false))
))
}

if (required) {
schema = schema.required()
}

return schema
}

const schema = Joi.object().keys({
acknowledgement: array.items(string),
contact: array.required().items(string.required()),
permission: string.only('none').insensitive(),
encryption: array.items(string.regex(/^(?!http:)/i)),
policy: array.items(string),
hiring: array.items(string),
signature: string
_prefixComment: comment,
acknowledgement: fieldValue(),
contact: fieldValue({ required: true }),
permission: fieldValue({ canBeArray: false, singleValue: string.only('none').insensitive() }),
encryption: fieldValue({ singleValue: string.regex(/^(?!http:)/i) }),
policy: fieldValue(),
hiring: fieldValue(),
signature: fieldValue({ canBeArray: false }),
_postfixComment: comment
}).label('options').required()

const result = Joi.validate(options, schema)
Expand Down

0 comments on commit aa23c88

Please sign in to comment.