Skip to content

Commit

Permalink
Enable substitions for simple secret hiding
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenprater committed Nov 17, 2023
1 parent 36a13fd commit da7e84a
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 18 deletions.
14 changes: 14 additions & 0 deletions README.md
Expand Up @@ -1497,6 +1497,20 @@ To set the mode call `nockBack.setMode(mode)` or run the tests with the `NOCK_BA

- lockdown: use recorded nocks, disables all http calls even when not nocked, doesn't record


### Hiding Secrets from NockBack

Nockback will happily record secrets that appear in URL parameters or HTTP request bodies. To prevent this you can use simple substitution to dynamically replace these values whenever the fixture is written or read from disk. To do this, use the `substitutions` options on Nock.

```js
context { nockDone } = nockBack('exampleFixture.json', { substitutions: { A_SECRET: "the-value-of-your-secret" }})
```

In the recorded cassette where ever `the-value-of-your-secret` would appear `{{ A_SECRET }}` will appear instead.

This feature should only be used for strings that are unlikely to occur naturally - as this works with simple string substitution.


## Common issues

**"No match for response" when using got with error responses**
Expand Down
35 changes: 22 additions & 13 deletions lib/back.js
Expand Up @@ -27,21 +27,22 @@ try {
/**
* nock the current function with the fixture given
*
* @param {string} fixtureName - the name of the fixture, e.x. 'foo.json'
* @param {object} options - [optional] extra options for nock with, e.x. `{ assert: true }`
* @param {function} nockedFn - [optional] callback function to be executed with the given fixture being loaded;
* if defined the function will be called with context `{ scopes: loaded_nocks || [] }`
* set as `this` and `nockDone` callback function as first and only parameter;
* if not defined a promise resolving to `{nockDone, context}` where `context` is
* aforementioned `{ scopes: loaded_nocks || [] }`
* @param {string} fixtureName - the name of the fixture, e.x. 'foo.json'
* @param {object} options - [optional] extra options for nock with, e.x. `{ assert: true }`
* @param {function} nockedFn - [optional] callback function to be executed with the given fixture being loaded;
* if defined the function will be called with context `{ scopes: loaded_nocks || [] }`
* set as `this` and `nockDone` callback function as first and only parameter;
* if not defined a promise resolving to `{nockDone, context}` where `context` is
* aforementioned `{ scopes: loaded_nocks || [] }`
*
* List of options:
*
* @param {function} before - a preprocessing function, gets called before nock.define
* @param {function} after - a postprocessing function, gets called after nock.define
* @param {function} afterRecord - a postprocessing function, gets called after recording. Is passed the array
* of scopes recorded and should return the array scopes to save to the fixture
* @param {function} recorder - custom options to pass to the recorder
* @param {object} substitutions - a list of key/value pairs to use as substitutions in a templated fixture
* @param {function} before - a preprocessing function, gets called before nock.define
* @param {function} after - a postprocessing function, gets called after nock.define
* @param {function} afterRecord - a postprocessing function, gets called after recording. Is passed the array
* of scopes recorded and should return the array scopes to save to the fixture
* @param {function} recorder - custom options to pass to the recorder
*
*/
function Back(fixtureName, options, nockedFn) {
Expand Down Expand Up @@ -167,6 +168,14 @@ const record = {

outputs =
typeof outputs === 'string' ? outputs : JSON.stringify(outputs, null, 4)

if(options.substitutions) {
Object.entries(options.substitutions).forEach((pair) => {
const [key, value] = pair
outputs = outputs.replace(new RegExp(value, 'g'), '{{ ' + key + ' }}')

Check failure on line 175 in lib/back.js

View workflow job for this annotation

GitHub Actions / Lint JavaScript

Unexpected string concatenation
})
}

debug('recorder outputs:', outputs)

fs.mkdirSync(path.dirname(fixture), { recursive: true })
Expand Down Expand Up @@ -243,7 +252,7 @@ function load(fixture, options) {
}

if (fixture && fixtureExists(fixture)) {
let scopes = loadDefs(fixture)
let scopes = loadDefs(fixture, options.substitutions)
applyHook(scopes, options.before)

scopes = define(scopes)
Expand Down
10 changes: 6 additions & 4 deletions lib/scope.js
Expand Up @@ -10,6 +10,7 @@ const url = require('url')
const debug = require('debug')('nock.scope')
const { EventEmitter } = require('events')
const Interceptor = require('./interceptor')
const Mustache = require('mustache')

const { URL, Url: LegacyUrl } = url
let fs
Expand Down Expand Up @@ -288,17 +289,18 @@ class Scope extends EventEmitter {
}
}

function loadDefs(path) {
function loadDefs(path, view = {}) {
if (!fs) {
throw new Error('No fs')
}

const contents = fs.readFileSync(path)
return JSON.parse(contents)
const rendered = Mustache.render(contents.toString(), view)
return JSON.parse(rendered)
}

function load(path) {
return define(loadDefs(path))
function load(path, view = {}) {
return define(loadDefs(path, view))
}

function getStatusFromDefinition(nockDef) {
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -24,6 +24,7 @@
"dependencies": {
"debug": "^4.1.0",
"json-stringify-safe": "^5.0.1",
"mustache": "^4.2.0",
"propagate": "^2.0.0"
},
"devDependencies": {
Expand Down
21 changes: 21 additions & 0 deletions tests/fixtures/test-template-substitution-fixture.json
@@ -0,0 +1,21 @@
[
{
"scope": "http://example.test:80",
"method": "GET",
"path": "/?secret={{ SECRET }}",
"body": "",
"status": 217,
"response": "server served a response",
"rawHeaders": [
"Date",
"Fri, 17 Nov 2023 03:19:12 GMT",
"Connection",
"keep-alive",
"Keep-Alive",
"timeout=5",
"Transfer-Encoding",
"chunked"
],
"responseIsBinary": false
}
]
51 changes: 50 additions & 1 deletion tests/test_back.js
Expand Up @@ -115,6 +115,21 @@ describe('Nock Back', () => {
})
})

it('should allow template substitutions in recorded fixtures', done => {
const mySecretApiKey = "blah-blah";

nockBack('test-template-substitution-fixture.json', {
substitutions: { SECRET: mySecretApiKey },
after: (scope) => {
expect(scope.interceptors[0].uri).to.eql("/?secret=blah-blah")
}}, function (nockDone) {
http.get('http://example.test/?secret=blah-blah', () => {
nockDone();
done();
});
})
})

it('should throw an exception when a hook is not a function', () => {
expect(() =>
nockBack('good_request.json', { before: 'not-a-function-innit' }),
Expand Down Expand Up @@ -271,7 +286,7 @@ describe('Nock Back', () => {
},
response => {
nockDone()

const fixtureContent = fs.readFileSync(fixtureLoc, 'utf8')

Check failure on line 289 in tests/test_back.js

View workflow job for this annotation

GitHub Actions / Lint JavaScript

'fixtureContent' is assigned a value but never used
expect(response.statusCode).to.equal(217)
expect(fs.existsSync(fixtureLoc)).to.be.true()
done()
Expand All @@ -284,6 +299,40 @@ describe('Nock Back', () => {
})
})

it('should record template keys into fixtures rather than secrets', done => {
expect(fs.existsSync(fixtureLoc)).to.be.false()

const mySecretApiKey = "sooper-secret"

nockBack(fixture, {
substitutions: { SECRET: mySecretApiKey },
}, function (nockDone) {
startHttpServer(requestListener).then(server => {
const request = http.request(
{
host: 'localhost',
path: '/?secret=sooper-secret',
port: server.address().port,
},
response => {
response.once('end', () => {
nockDone()
const fixtureContent = fs.readFileSync(fixtureLoc, 'utf8')
expect(response.statusCode).to.equal(217)
expect(fixtureContent).to.contain('{{ SECRET }}')
done()
})

response.resume()
},
)

request.on('error', () => expect.fail())
request.end()
})
})
})

it('should record the expected data', done => {
nockBack(fixture, nockDone => {
startHttpServer(requestListener).then(server => {
Expand Down
1 change: 1 addition & 0 deletions types/index.d.ts
Expand Up @@ -274,6 +274,7 @@ declare namespace nock {
}

interface BackOptions {
substitutions?: { [key: string]: string }
before?: (def: Definition) => void
after?: (scope: Scope) => void
afterRecord?: (defs: Definition[]) => Definition[]
Expand Down

0 comments on commit da7e84a

Please sign in to comment.