@contrast/assess 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/lib/constants.js +26 -0
  2. package/lib/crypto-analysis/common.js +20 -0
  3. package/lib/crypto-analysis/index.js +44 -0
  4. package/lib/crypto-analysis/install/crypto.js +151 -0
  5. package/lib/crypto-analysis/install/math.js +99 -0
  6. package/lib/dataflow/propagation/install/JSON/parse.js +12 -11
  7. package/lib/dataflow/propagation/install/JSON/stringify.js +1 -1
  8. package/lib/dataflow/propagation/install/ejs/escape-xml.js +2 -2
  9. package/lib/dataflow/propagation/install/ejs/index.js +1 -0
  10. package/lib/dataflow/propagation/install/ejs/template.js +77 -0
  11. package/lib/dataflow/propagation/install/util-format.js +9 -3
  12. package/lib/dataflow/sinks/install/child-process.js +20 -14
  13. package/lib/dataflow/sinks/install/eval.js +16 -14
  14. package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +14 -8
  15. package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +12 -5
  16. package/lib/dataflow/sinks/install/fs.js +7 -7
  17. package/lib/dataflow/sinks/install/function.js +8 -12
  18. package/lib/dataflow/sinks/install/http/request.js +16 -8
  19. package/lib/dataflow/sinks/install/http/server-response.js +11 -2
  20. package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +15 -8
  21. package/lib/dataflow/sinks/install/libxmljs.js +15 -10
  22. package/lib/dataflow/sinks/install/marsdb.js +13 -8
  23. package/lib/dataflow/sinks/install/mongodb.js +25 -15
  24. package/lib/dataflow/sinks/install/mssql.js +20 -9
  25. package/lib/dataflow/sinks/install/mysql.js +15 -8
  26. package/lib/dataflow/sinks/install/node-serialize.js +15 -17
  27. package/lib/dataflow/sinks/install/postgres.js +17 -4
  28. package/lib/dataflow/sinks/install/sequelize.js +16 -9
  29. package/lib/dataflow/sinks/install/sqlite3.js +20 -7
  30. package/lib/dataflow/sinks/install/vm.js +19 -17
  31. package/lib/dataflow/sources/install/http.js +14 -42
  32. package/lib/dataflow/sources/install/koa/index.js +1 -0
  33. package/lib/dataflow/sources/install/koa/koa-multer.js +102 -0
  34. package/lib/dataflow/sources/install/multer1.js +25 -51
  35. package/lib/dataflow/sources/install/querystring.js +1 -4
  36. package/lib/event-factory.js +47 -0
  37. package/lib/get-policy.js +68 -0
  38. package/lib/get-source-context.js +62 -0
  39. package/lib/index.d.ts +50 -0
  40. package/lib/index.js +20 -19
  41. package/lib/make-source-context.js +74 -0
  42. package/lib/response-scanning/handlers/index.js +55 -28
  43. package/lib/response-scanning/install/http.js +13 -7
  44. package/lib/rule-scopes.js +48 -0
  45. package/lib/session-configuration/handlers.js +4 -3
  46. package/lib/session-configuration/install/express-session.js +8 -2
  47. package/package.json +2 -2
@@ -23,36 +23,20 @@ module.exports = (core) => {
23
23
  depHooks,
24
24
  patcher,
25
25
  logger,
26
- assess,
27
- scopes: { wrap },
26
+ assess: { dataflow: { sources } },
27
+ scopes,
28
28
  } = core;
29
29
 
30
- function handleFile(sourceContext, constructorOpt, req) {
31
- try {
32
- assess.dataflow.sources.handle({
33
- context: 'req',
34
- data: req,
35
- keys: ['file'],
36
- name: 'multer',
37
- inputType: InputType.BODY,
38
- sourceContext,
39
- stacktraceOpts: {
40
- constructorOpt,
41
- },
42
- });
43
- } catch (err) {
44
- logger.error({ err }, 'error handling multer Assess dataflow req.file source');
45
- }
46
- }
30
+ function handler(req, constructorOpt) {
31
+ const sourceContext = scopes.sources.getStore()?.assess;
32
+ if (!sourceContext) return;
47
33
 
48
- function handleFiles(sourceContext, constructorOpt, req) {
49
- let i;
50
- for (i = 0; i < req.files.length; i++) {
34
+ function handle(context, data, key) {
51
35
  try {
52
- assess.dataflow.sources.handle({
53
- context: 'req.files',
54
- data: req.files[i],
55
- keys: [i],
36
+ sources.handle({
37
+ context,
38
+ data,
39
+ keys: [key],
56
40
  name: 'multer',
57
41
  inputType: InputType.BODY,
58
42
  sourceContext,
@@ -61,26 +45,22 @@ module.exports = (core) => {
61
45
  },
62
46
  });
63
47
  } catch (err) {
64
- logger.error({ err }, 'error handling multer Assess dataflow req.files[%s] source', i);
48
+ logger.error({ err }, 'error handling multer Assess dataflow %s.%s source', context, key);
65
49
  }
66
50
  }
67
- }
68
51
 
69
- function handleBody(sourceContext, constructorOpt, req) {
70
- try {
71
- assess.dataflow.sources.handle({
72
- context: 'req',
73
- data: req,
74
- keys: ['body'],
75
- name: 'multer',
76
- inputType: InputType.BODY,
77
- sourceContext,
78
- stacktraceOpts: {
79
- constructorOpt,
80
- },
81
- });
82
- } catch (err) {
83
- logger.error({ err }, 'error handling multer Assess dataflow req.body source');
52
+ if (req.file) {
53
+ handle('req', req, 'file');
54
+ }
55
+
56
+ if (Array.isArray(req.files)) {
57
+ for (let i = 0; i < req.files.length; i++) {
58
+ handle('req.files', req.files[i], i);
59
+ }
60
+ }
61
+
62
+ if (req.body && Object.keys(req.body).length) {
63
+ handle('req', req, 'body');
84
64
  }
85
65
  }
86
66
 
@@ -97,14 +77,8 @@ module.exports = (core) => {
97
77
  patchType,
98
78
  pre(data) {
99
79
  const [req, , next, hooked] = data.args;
100
-
101
- const sourceContext = core.scopes.sources.getStore()?.assess;
102
- if (!sourceContext) return;
103
-
104
- data.args[2] = wrap(function (...args) {
105
- if (req.file) handleFile(sourceContext, hooked, req);
106
- if (Array.isArray(req.files)) handleFiles(sourceContext, hooked, req);
107
- if (req.body && Object.keys(req.body).length) handleBody(sourceContext, hooked, req.body);
80
+ data.args[2] = scopes.wrap(function (...args) {
81
+ handler(req, hooked);
108
82
  next(...args);
109
83
  });
110
84
  },
@@ -32,10 +32,7 @@ module.exports = (core) => {
32
32
  const sourceContext = core.scopes.sources.getStore()?.assess;
33
33
  const inputType = InputType.QUERYSTRING;
34
34
 
35
- if (!sourceContext) {
36
- logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
37
- return;
38
- }
35
+ if (!sourceContext) return;
39
36
 
40
37
  if (sourceContext.parsedQuery) {
41
38
  logger.trace({ name }, 'values already tracked');
@@ -270,5 +270,52 @@ module.exports = function(core) {
270
270
  return event;
271
271
  };
272
272
 
273
+
274
+ /**
275
+ * @param {{
276
+ * context: string,
277
+ * name: string,
278
+ * moduleName: string,
279
+ * methodName: string,
280
+ * object: { value: any, tracked: boolean },
281
+ * args: any[],
282
+ * result: { value: vany, tracked: boolean },
283
+ * source: string,
284
+ * stacktraceOpts: { constructorOpt?: Function},
285
+ * }} data
286
+ * @returns {any}
287
+ */
288
+ eventFactory.createCryptoAnalysisEvent = function(data) {
289
+ const {
290
+ name = '',
291
+ source,
292
+ stacktraceOpts,
293
+ } = data;
294
+
295
+ if (!name) {
296
+ logger.debug({ data }, 'no sink event name');
297
+ return null;
298
+ }
299
+
300
+ if (!source || !source.match(annotationRegExp)) {
301
+ logger.debug({ data }, 'malformed or missing sink event source field');
302
+ return null;
303
+ }
304
+
305
+ let stack;
306
+ if (config.assess.stacktraces !== 'NONE') {
307
+ stack = createSnapshot(stacktraceOpts)();
308
+ } else {
309
+ stack = [];
310
+ }
311
+
312
+ data.stack = stack;
313
+ data.time = Date.now();
314
+
315
+ eventFactory.createdEvents.add(data);
316
+
317
+ return data;
318
+ };
319
+
273
320
  return eventFactory;
274
321
  };
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright: 2023 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const {
19
+ Rule,
20
+ ResponseScanningRule,
21
+ SessionConfigurationRule,
22
+ Event,
23
+ } = require('@contrast/common');
24
+
25
+ const rulesIds = Object.values({
26
+ ...Rule,
27
+ ...ResponseScanningRule,
28
+ ...SessionConfigurationRule,
29
+ });
30
+
31
+ /**
32
+ * @param {{
33
+ * config: import('@contrast/config').Config,
34
+ * logger: import('@contrast/logger').Logger,
35
+ * messages: import('@contrast/common').Messages,
36
+ * }} core
37
+ * @returns {import('@contrast/common').Installable}
38
+ */
39
+ module.exports = function assess(core) {
40
+ const { logger, messages } = core;
41
+
42
+ const enabledRules = new Set(rulesIds);
43
+
44
+ messages.on(Event.SERVER_SETTINGS_UPDATE, (msg) => {
45
+ if (!msg.assess) return;
46
+
47
+ for (const ruleId of rulesIds) {
48
+ const enable = msg.assess[ruleId]?.enable;
49
+ if (enable === true) {
50
+ enabledRules.add(ruleId);
51
+ if (ruleId === Rule.NOSQL_INJECTION) enabledRules.add(Rule.NOSQL_INJECTION_MONGO);
52
+ } else if (enable === false) {
53
+ if (ruleId === Rule.NOSQL_INJECTION) enabledRules.delete(Rule.NOSQL_INJECTION_MONGO);
54
+ enabledRules.delete(ruleId);
55
+ }
56
+ }
57
+ logger.info({
58
+ enabledRules: Array.from(enabledRules)
59
+ }, 'Assess policy updated');
60
+ });
61
+
62
+ return core.assess.getPolicy = function getPolicy() {
63
+ // creates copy of local policy for request store
64
+ return {
65
+ enabledRules: new Set(enabledRules),
66
+ };
67
+ };
68
+ };
@@ -0,0 +1,62 @@
1
+ /*
2
+ * Copyright: 2023 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const { InstrumentationType } = require('./constants');
19
+
20
+ /**
21
+ * @param {{
22
+ * assess: import('@contrast/assess').Assess,
23
+ * config: import('@contrast/config').Config,
24
+ * scopes: import('@contrast/scopes').Scopes,
25
+ * }} core
26
+ */
27
+ module.exports = function(core) {
28
+ const {
29
+ config,
30
+ scopes: { sources, instrumentation },
31
+ assess: { ruleScopes },
32
+ } = core;
33
+
34
+ /**
35
+ * @param {import('./constants.js').InstrumentationType} type
36
+ * @returns {import('@contrast/assess').SourceContext|null} the assess store
37
+ */
38
+ return core.assess.getSourceContext = function getSourceContext(type, ...rest) {
39
+ const ctx = sources.getStore()?.assess;
40
+
41
+ if (!ctx || instrumentation.isLocked()) return null;
42
+
43
+ switch (type) {
44
+ case InstrumentationType.PROPAGATOR: {
45
+ if (ctx.propagationEventsCount > config.assess.max_propagation_events) return null;
46
+ break;
47
+ }
48
+ case InstrumentationType.SOURCE: {
49
+ if (ctx.sourceEventsCount > config.assess.max_context_source_events) return null;
50
+ break;
51
+ }
52
+ case InstrumentationType.RULE: {
53
+ const [ruleId] = rest;
54
+ if (!ruleId) break;
55
+ if (!ctx.policy.enabledRules.has(ruleId) || ruleScopes.isLocked(ruleId)) return null;
56
+ break;
57
+ }
58
+ }
59
+
60
+ return ctx;
61
+ };
62
+ };
package/lib/index.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ /*
2
+ * Copyright: 2023 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+ import { IncomingMessage, ServerResponse } from 'node:http';
16
+ import { Rule } from '@contrast/common';
17
+
18
+ export enum InstrumentationType {
19
+ SOURCE = 'source',
20
+ PROPAGATOR = 'propagator',
21
+ RULE = 'rule',
22
+ }
23
+
24
+ export interface SourceContext {
25
+ reqData: object,
26
+ responseData: {
27
+ contentType: string,
28
+ },
29
+ policy: {
30
+ enabledRules: Set<string>,
31
+ }
32
+ }
33
+
34
+ export interface Policy {
35
+ enabledRules: Set<Rule>,
36
+ }
37
+
38
+ export interface RuleScopes {
39
+ run(ruleId: Rule, cb: void): void;
40
+ isLocked(ruleId: Rule): boolean;
41
+ }
42
+
43
+ export interface Assess {
44
+ getPolicy(): Policy,
45
+ getSourceContext(instrType?: InstrumentationType, opts?: any): SourceContext,
46
+ makeSourceContext(req: IncomingMessage, res: ServerResponse): SourceContext,
47
+ ruleScopes: RuleScopes,
48
+ }
49
+
50
+ export function getSourceContext(instrType?: InstrumentationType, ops?: any): SourceContext;
package/lib/index.js CHANGED
@@ -16,32 +16,33 @@
16
16
  'use strict';
17
17
 
18
18
  const { callChildComponentMethodsSync } = require('@contrast/common');
19
- const sessionConfiguration = require('./session-configuration');
20
- const dataflow = require('./dataflow');
21
- const responseScanning = require('./response-scanning');
22
- const eventFactory = require('./event-factory');
23
19
 
24
20
  module.exports = function assess(core) {
25
- const assess = core.assess = {};
21
+ const assess = core.assess = {
22
+ install() {
23
+ if (!core.config.getEffectiveValue('assess.enable')) {
24
+ core.logger.debug('assess is disabled, skipping installation');
25
+ return;
26
+ }
27
+
28
+ core.rewriter.install('assess');
29
+ callChildComponentMethodsSync(core.assess, 'install');
30
+ },
31
+ };
26
32
 
27
- eventFactory(core);
28
- dataflow(core);
29
- responseScanning(core);
30
- sessionConfiguration(core);
33
+ require('./rule-scopes')(core);
34
+ require('./get-policy')(core);
35
+ require('./make-source-context')(core);
36
+ require('./get-source-context')(core);
37
+ require('./event-factory')(core);
38
+ require('./crypto-analysis')(core);
39
+ require('./dataflow')(core);
40
+ require('./response-scanning')(core);
41
+ require('./session-configuration')(core);
31
42
 
32
43
  // todo
33
44
  // crypto rule implementations
34
45
  // static rule implementations in coordination with (CLI) rewriter
35
46
 
36
- assess.install = function() {
37
- if (!core.config.getEffectiveValue('assess.enable')) {
38
- core.logger.debug('assess is disabled, skipping installation');
39
- return;
40
- }
41
-
42
- core.rewriter.install('assess');
43
- callChildComponentMethodsSync(core.assess, 'install');
44
- };
45
-
46
47
  return assess;
47
48
  };
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Copyright: 2023 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const { toLowerCase } = require('@contrast/common');
19
+
20
+ /**
21
+ * @param {{
22
+ * assess: import('@contrast/assess').Assess,
23
+ * logger: import('@contrast/logger').Logger,
24
+ * }} core
25
+ */
26
+ module.exports = function(core) {
27
+ const {
28
+ assess: { getPolicy },
29
+ logger,
30
+ } = core;
31
+
32
+ return core.assess.makeSourceContext = function (req, res) {
33
+ let contentType, queries, uriPath;
34
+
35
+ try {
36
+ const ix = req.url.indexOf('?');
37
+ if (ix >= 0) {
38
+ uriPath = req.url.slice(0, ix);
39
+ queries = req.url.slice(ix + 1);
40
+ } else {
41
+ uriPath = req.url;
42
+ queries = '';
43
+ }
44
+
45
+ // copy to avoid storing tracked values
46
+ const headers = { ...req.headers };
47
+ if (headers['content-type']) {
48
+ contentType = toLowerCase(headers['content-type']);
49
+ }
50
+
51
+ return {
52
+ propagationEventsCount: 0,
53
+ sourceEventsCount: 0,
54
+ policy: getPolicy(),
55
+ reqData: {
56
+ ip: req.socket.remoteAddress,
57
+ httpVersion: req.httpVersion,
58
+ method: req.method,
59
+ headers,
60
+ uriPath,
61
+ queries,
62
+ contentType,
63
+ },
64
+ responseData: {}
65
+ };
66
+ } catch (err) {
67
+ logger.error(
68
+ { err },
69
+ 'unable to construct assess store. assess will be disabled for request.'
70
+ );
71
+ return null;
72
+ }
73
+ };
74
+ };
@@ -33,6 +33,19 @@ const {
33
33
  checkCspSources
34
34
  } = require('./utils');
35
35
 
36
+ const {
37
+ AUTOCOMPLETE_MISSING,
38
+ CACHE_CONTROLS_MISSING,
39
+ CLICKJACKING_CONTROL_MISSING,
40
+ CSP_HEADER_MISSING,
41
+ CSP_HEADER_INSECURE,
42
+ HSTS_HEADER_MISSING,
43
+ PARAMETER_POLLUTION,
44
+ XCONTENTTYPE_HEADER_MISSING,
45
+ X_POWERED_BY_HEADER,
46
+ XXSPROTECTION_HEADER_DISABLED,
47
+ } = ResponseScanningRule;
48
+
36
49
  module.exports = function(core) {
37
50
  const {
38
51
  assess: {
@@ -44,7 +57,10 @@ module.exports = function(core) {
44
57
  } = core;
45
58
 
46
59
  responseScanning.handleAutoCompleteMissing = function(sourceContext, { responseHeaders, responseBody }) {
47
- if (!isHtmlContent(responseHeaders)) {
60
+ if (
61
+ !isEnabled(AUTOCOMPLETE_MISSING, sourceContext) ||
62
+ !isHtmlContent(responseHeaders)
63
+ ) {
48
64
  return;
49
65
  }
50
66
 
@@ -72,7 +88,10 @@ module.exports = function(core) {
72
88
  const instructions = [];
73
89
 
74
90
  // de-dupe; this will be re-emitted for parseableBody handlers anyway
75
- if (isParseableResponse(responseHeaders) && !responseBody) {
91
+ if (
92
+ !isEnabled(CACHE_CONTROLS_MISSING, sourceContext) ||
93
+ (isParseableResponse(responseHeaders) && !responseBody)
94
+ ) {
76
95
  return;
77
96
  }
78
97
  const cacheControlHeader = responseHeaders['cache-control'];
@@ -118,6 +137,8 @@ module.exports = function(core) {
118
137
  };
119
138
 
120
139
  responseScanning.handleClickJackingControlsMissing = function(sourceContext, { responseHeaders }) {
140
+ if (!isEnabled(CLICKJACKING_CONTROL_MISSING, sourceContext)) return;
141
+
121
142
  // look for x-frame-options headers with deny or sameorigin
122
143
  const xFrameHeaders = responseHeaders['x-frame-options'];
123
144
  let hasFrameBusting = false;
@@ -135,6 +156,8 @@ module.exports = function(core) {
135
156
  };
136
157
 
137
158
  responseScanning.handleParameterPollution = function(sourceContext, { responseBody }) {
159
+ if (!isEnabled(PARAMETER_POLLUTION, sourceContext)) return;
160
+
138
161
  // look for form tag with missing action attribute.
139
162
  // ex: <form method="post">..
140
163
  const elements = getElements('form', responseBody);
@@ -154,21 +177,23 @@ module.exports = function(core) {
154
177
  };
155
178
 
156
179
  /**
157
- * Checks the response headers for the CSP. If found, and insecure, will return
158
- * the evidence for reporting.
159
- *
160
- * @param {Object} responseHeaders - HTTP headers object.
161
- * @returns {Object} - Evidence for insecure CSP header.
162
- */
180
+ * Checks the response headers for the CSP. If found, and insecure, will return
181
+ * the evidence for reporting.
182
+ *
183
+ * @param {Object} responseHeaders - HTTP headers object.
184
+ * @returns {Object} - Evidence for insecure CSP header.
185
+ */
163
186
  responseScanning.handleCspHeader = function(sourceContext, { responseHeaders }) {
164
187
  const cspHeaders = getCspHeaders(responseHeaders);
165
188
 
166
189
  // Don't report if not set; this report belongs to 'csp-header-missing'
167
- if (!cspHeaders) {
190
+ if (!cspHeaders && isEnabled(CSP_HEADER_MISSING, sourceContext)) {
168
191
  reportFindings(sourceContext, { ruleId: ResponseScanningRule.CSP_HEADER_MISSING });
169
192
  return;
170
193
  }
171
194
 
195
+ if (!isEnabled(CSP_HEADER_INSECURE, sourceContext)) return;
196
+
172
197
  const vulnerabilityMetadata = checkCspSources(cspHeaders);
173
198
 
174
199
  if (vulnerabilityMetadata.insecure) {
@@ -189,6 +214,8 @@ module.exports = function(core) {
189
214
  };
190
215
 
191
216
  responseScanning.handleHstsHeaderMissing = function(sourceContext, { responseHeaders }) {
217
+ if (!isEnabled(HSTS_HEADER_MISSING, sourceContext)) return;
218
+
192
219
  let header = responseHeaders['strict-transport-security'];
193
220
  let maxAge;
194
221
 
@@ -219,6 +246,8 @@ module.exports = function(core) {
219
246
  };
220
247
 
221
248
  responseScanning.handleXContentTypeHeaderMissing = function(sourceContext, { responseHeaders }) {
249
+ if (!isEnabled(XCONTENTTYPE_HEADER_MISSING, sourceContext)) return;
250
+
222
251
  const headerName = 'x-content-type-options';
223
252
  let header = responseHeaders[headerName];
224
253
 
@@ -237,44 +266,42 @@ module.exports = function(core) {
237
266
  });
238
267
  };
239
268
 
240
- // NODE-3135
241
269
  responseScanning.handleXPoweredByHeader = function(sourceContext, { responseHeaders }) {
270
+ if (!isEnabled(X_POWERED_BY_HEADER, sourceContext)) return;
271
+
242
272
  const headerName = 'x-powered-by';
243
273
  let header = responseHeaders[headerName];
244
274
 
245
275
  if (header) {
246
276
  header = toLowerCase(header);
247
277
 
248
- const instructions = [
249
- {
250
- type: 'Header',
251
- name: headerName,
252
- value: header
253
- }
254
- ];
255
-
256
278
  reportFindings(sourceContext, {
257
279
  ruleId: ResponseScanningRule.X_POWERED_BY_HEADER,
258
280
  vulnerabilityMetadata: {
259
- data: JSON.stringify(instructions)
281
+ platform: header,
260
282
  }
261
283
  });
262
284
  }
263
285
  };
264
286
 
265
287
  responseScanning.handleXxsProtectionHeaderDisabled = function(sourceContext, { responseHeaders }) {
266
- const header = responseHeaders['x-xss-protection'];
288
+ if (!isEnabled(XXSPROTECTION_HEADER_DISABLED, sourceContext)) return;
267
289
 
268
- if (header?.startsWith?.('1')) return;
290
+ const header = responseHeaders['x-xss-protection'];
269
291
 
270
- reportFindings(sourceContext, {
271
- ruleId: ResponseScanningRule.XXSPROTECTION_HEADER_DISABLED,
272
- vulnerabilityMetadata: {
273
- data: header,
274
- }
275
- });
292
+ if (header && !header.startsWith('1')) {
293
+ reportFindings(sourceContext, {
294
+ ruleId: ResponseScanningRule.XXSPROTECTION_HEADER_DISABLED,
295
+ vulnerabilityMetadata: {
296
+ data: header,
297
+ }
298
+ });
299
+ }
276
300
  };
277
301
 
302
+ function isEnabled(ruleId, sourceContext) {
303
+ return !!sourceContext?.policy?.enabledRules?.has?.(ruleId);
304
+ }
305
+
278
306
  return responseScanning;
279
307
  };
280
-