-
Notifications
You must be signed in to change notification settings - Fork 306
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add support to api security sampling
- Loading branch information
1 parent
fcd3ab8
commit 6987a66
Showing
15 changed files
with
257 additions
and
341 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -115,7 +115,6 @@ tracer.init({ | |
}, | ||
apiSecurity: { | ||
enabled: true, | ||
requestSampling: 1.0 | ||
}, | ||
rasp: { | ||
enabled: true | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,61 +1,51 @@ | ||
'use strict' | ||
|
||
const log = require('../log') | ||
const ApiSecuritySamplerCache = require('./api_security_sampler_cache') | ||
const web = require('../plugins/util/web') | ||
const { USER_KEEP, AUTO_KEEP } = require('../../../../ext/priority') | ||
|
||
let enabled | ||
let requestSampling | ||
|
||
const sampledRequests = new WeakSet() | ||
let sampledRequests | ||
|
||
function configure ({ apiSecurity }) { | ||
enabled = apiSecurity.enabled | ||
setRequestSampling(apiSecurity.requestSampling) | ||
sampledRequests = new ApiSecuritySamplerCache(apiSecurity.sampleDelay) | ||
} | ||
|
||
function disable () { | ||
enabled = false | ||
sampledRequests?.clear() | ||
} | ||
|
||
function setRequestSampling (sampling) { | ||
requestSampling = parseRequestSampling(sampling) | ||
} | ||
|
||
function parseRequestSampling (requestSampling) { | ||
let parsed = parseFloat(requestSampling) | ||
|
||
if (isNaN(parsed)) { | ||
log.warn(`Incorrect API Security request sampling value: ${requestSampling}`) | ||
function sampleRequest (req, res) { | ||
if (!enabled) return false | ||
|
||
parsed = 0 | ||
} else { | ||
parsed = Math.min(1, Math.max(0, parsed)) | ||
} | ||
const rootSpan = web.root(req) | ||
if (!rootSpan) return false | ||
|
||
return parsed | ||
} | ||
const priority = getSpanPriority(rootSpan) | ||
|
||
function sampleRequest (req) { | ||
if (!enabled || !requestSampling) { | ||
if (priority !== AUTO_KEEP && priority !== USER_KEEP) { | ||
return false | ||
} | ||
|
||
const shouldSample = Math.random() <= requestSampling | ||
const key = sampledRequests.computeKey(req, res) | ||
const isSampled = sampledRequests.isSampled(key) | ||
|
||
if (shouldSample) { | ||
sampledRequests.add(req) | ||
} | ||
if (isSampled) return false | ||
|
||
sampledRequests.set(key) | ||
|
||
return shouldSample | ||
return true | ||
} | ||
|
||
function isSampled (req) { | ||
return sampledRequests.has(req) | ||
function getSpanPriority (span) { | ||
const spanContext = span.context?.() | ||
return spanContext._sampling?.priority // default ?? | ||
} | ||
|
||
module.exports = { | ||
configure, | ||
disable, | ||
setRequestSampling, | ||
sampleRequest, | ||
isSampled | ||
sampleRequest | ||
} |
53 changes: 53 additions & 0 deletions
53
packages/dd-trace/src/appsec/api_security_sampler_cache.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
'use strict' | ||
|
||
const crypto = require('node:crypto') | ||
const log = require('../log') | ||
|
||
const MAX_SIZE = 4096 | ||
const DEFAULT_DELAY = 30 // 30s | ||
|
||
class ApiSecuritySamplerCache extends Map { | ||
constructor (delay) { | ||
super() | ||
this.delay = this._parseSampleDelay(delay) | ||
} | ||
|
||
_parseSampleDelay (delay) { | ||
if (typeof delay === 'number' && Number.isFinite(delay) && delay > 0) { | ||
return delay | ||
} else { | ||
log.warn('Invalid delay value. Delay must be a positive number.') | ||
return DEFAULT_DELAY | ||
} | ||
} | ||
|
||
computeKey (req, res) { | ||
const route = req.url | ||
const method = req.method.toLowerCase() | ||
const statusCode = res.statusCode | ||
const str = route + statusCode + method | ||
return crypto.createHash('md5').update(str).digest('hex') | ||
} | ||
|
||
isSampled (key) { | ||
if (!super.has(key)) { | ||
return false | ||
} | ||
const previous = super.get(key) | ||
return Date.now() - previous < (this.delay * 1000) | ||
} | ||
|
||
set (key) { | ||
if (super.has(key)) { | ||
super.delete(key) | ||
} | ||
|
||
super.set(key, Date.now()) | ||
if (super.size > MAX_SIZE) { | ||
const oldestKey = super.keys().next().value | ||
super.delete(oldestKey) | ||
} | ||
} | ||
} | ||
|
||
module.exports = ApiSecuritySamplerCache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 48 additions & 44 deletions
92
packages/dd-trace/test/appsec/api_security_sampler.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,71 +1,75 @@ | ||
'use strict' | ||
|
||
const apiSecuritySampler = require('../../src/appsec/api_security_sampler') | ||
const proxyquire = require('proxyquire') | ||
|
||
describe('Api Security Sampler', () => { | ||
let config | ||
describe('API Security Sampler', () => { | ||
let apiSecuritySampler, webStub, clock | ||
|
||
beforeEach(() => { | ||
config = { | ||
apiSecurity: { | ||
enabled: true, | ||
requestSampling: 1 | ||
} | ||
} | ||
|
||
sinon.stub(Math, 'random').returns(0.3) | ||
webStub = { root: sinon.stub() } | ||
clock = sinon.useFakeTimers(Date.now()) | ||
|
||
apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { | ||
'../plugins/util/web': webStub | ||
}) | ||
}) | ||
|
||
afterEach(sinon.restore) | ||
afterEach(() => { | ||
clock.restore() | ||
}) | ||
|
||
describe('sampleRequest', () => { | ||
it('should sample request if enabled and sampling 1', () => { | ||
apiSecuritySampler.configure(config) | ||
|
||
expect(apiSecuritySampler.sampleRequest({})).to.true | ||
beforeEach(() => { | ||
apiSecuritySampler.configure({ apiSecurity: { enabled: true, sampleDelay: 30 } }) | ||
}) | ||
|
||
it('should not sample request if enabled and sampling 0', () => { | ||
config.apiSecurity.requestSampling = 0 | ||
apiSecuritySampler.configure(config) | ||
|
||
expect(apiSecuritySampler.sampleRequest({})).to.false | ||
it('should return false if not enabled', () => { | ||
apiSecuritySampler.disable() | ||
expect(apiSecuritySampler.sampleRequest({}, {})).to.be.false | ||
}) | ||
|
||
it('should sample request if enabled and sampling greater than random', () => { | ||
config.apiSecurity.requestSampling = 0.5 | ||
|
||
apiSecuritySampler.configure(config) | ||
|
||
expect(apiSecuritySampler.sampleRequest({})).to.true | ||
it('should return false if no root span', () => { | ||
webStub.root.returns(null) | ||
expect(apiSecuritySampler.sampleRequest({}, {})).to.be.false | ||
}) | ||
|
||
it('should not sample request if enabled and sampling less than random', () => { | ||
config.apiSecurity.requestSampling = 0.1 | ||
|
||
apiSecuritySampler.configure(config) | ||
|
||
expect(apiSecuritySampler.sampleRequest()).to.false | ||
it('should return true and put request in cache if priority is AUTO_KEEP', () => { | ||
const rootSpan = { context: () => ({ _sampling: { priority: 2 } }) } | ||
webStub.root.returns(rootSpan) | ||
const req = { url: '/test', method: 'GET' } | ||
const res = { statusCode: 200 } | ||
expect(apiSecuritySampler.sampleRequest(req, res)).to.be.true | ||
}) | ||
|
||
it('should not sample request if incorrect config value', () => { | ||
config.apiSecurity.requestSampling = NaN | ||
|
||
apiSecuritySampler.configure(config) | ||
it('should return true and put request in cache if priority is USER_KEEP', () => { | ||
const rootSpan = { context: () => ({ _sampling: { priority: 1 } }) } | ||
webStub.root.returns(rootSpan) | ||
const req = { url: '/test', method: 'GET' } | ||
const res = { statusCode: 200 } | ||
expect(apiSecuritySampler.sampleRequest(req, res)).to.be.true | ||
}) | ||
|
||
expect(apiSecuritySampler.sampleRequest()).to.false | ||
it('should return false if priority is neither AUTO_KEEP nor USER_KEEP', () => { | ||
const rootSpan = { context: () => ({ _sampling: { priority: 0 } }) } | ||
webStub.root.returns(rootSpan) | ||
expect(apiSecuritySampler.sampleRequest({}, {})).to.be.false | ||
}) | ||
}) | ||
|
||
it('should sample request according to the config', () => { | ||
config.apiSecurity.requestSampling = 1 | ||
describe('disable', () => { | ||
it('should set enabled to false and clear the cache', () => { | ||
const req = { url: '/test', method: 'GET' } | ||
const res = { statusCode: 200 } | ||
|
||
apiSecuritySampler.configure(config) | ||
const rootSpan = { context: () => ({ _sampling: { priority: 2 } }) } | ||
webStub.root.returns(rootSpan) | ||
|
||
expect(apiSecuritySampler.sampleRequest({})).to.true | ||
apiSecuritySampler.configure({ apiSecurity: { enabled: true, sampleDelay: 30 } }) | ||
expect(apiSecuritySampler.sampleRequest(req, res)).to.be.true | ||
|
||
apiSecuritySampler.setRequestSampling(0) | ||
apiSecuritySampler.disable() | ||
|
||
expect(apiSecuritySampler.sampleRequest()).to.false | ||
expect(apiSecuritySampler.sampleRequest({}, {})).to.be.false | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.