Skip to content
This repository was archived by the owner on Oct 29, 2025. It is now read-only.

Commit b3ad189

Browse files
authored
Merge pull request #212 from PerimeterX/dev
Dev
2 parents f8556b9 + 23637c7 commit b3ad189

30 files changed

+524
-119
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## [3.2.0] - 2022-01-24
9+
10+
### Added
11+
12+
- Support for credentials intelligence protocols `v1` and `multistep_sso`
13+
- Support for login successful reporting methods `header`, `status`, and `custom`
14+
- Support for automatic sending of `additional_s2s` activity
15+
- Support for manual sending of `additional_s2s` activity via header or API call
16+
- Support for sending raw username on `additional_s2s` activity
17+
- Support for login credentials extraction via custom callback
18+
- New `request_id` field to all enforcer activities
19+
20+
### Changed
21+
22+
- Login credentials extraction handles body encoding based on `Content-Type` request header
23+
- Successful login credentials extraction automatically triggers risk_api call without needing to enable sensitive routes
24+
25+
### Fixed
26+
27+
- Enforced routes work in monitor mode
28+
- Bypass monitor header works with configured monitored routes
29+
830
## [3.1.2] - 2022-01-18
931

1032
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[PerimeterX](http://www.perimeterx.com) Shared base for NodeJS enforcers
77
=============================================================
88

9-
> Latest stable version: [v3.1.2](https://www.npmjs.com/package/perimeterx-node-core)
9+
> Latest stable version: [v3.2.0](https://www.npmjs.com/package/perimeterx-node-core)
1010
1111
This is a shared base implementation for PerimeterX Express enforcer and future NodeJS enforcers. For a fully functioning implementation example, see the [Node-Express enforcer](https://github.com/PerimeterX/perimeterx-node-express/) implementation.
1212

lib/enums/CIVersion.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const CIVersion = {
2+
V1: 'v1',
3+
MULTISTEP_SSO: 'multistep_sso'
4+
};
5+
6+
module.exports = {
7+
CIVersion
8+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const LoginSuccessfulReportingMethod = {
2+
NONE: '',
3+
HEADER: 'header',
4+
BODY: 'body',
5+
STATUS: 'status',
6+
CUSTOM: 'custom'
7+
};
8+
9+
module.exports = {
10+
LoginSuccessfulReportingMethod
11+
};

lib/enums/SSOStep.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const SSOStep = {
2+
USER: 'user',
3+
PASS: 'pass'
4+
};
5+
6+
module.exports = {
7+
SSOStep
8+
};

lib/extract_field/FieldExtractor.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,32 @@ const SentThrough = { BODY: 'body', QUERY_PARAM: 'query-param', HEADER: 'header'
55
const ContentType = { JSON: 'application/json', URL_ENCODED: 'application/x-www-form-urlencoded', MULTIPART_FORM: 'multipart/form-data' };
66

77
class FieldExtractor {
8-
constructor(sentThrough, fieldsToExtract) {
8+
constructor(sentThrough, customExtractionCallback, fieldsToExtract) {
99
this.containerName = this._getContainerName(sentThrough);
10+
this.customExtractionCallback = customExtractionCallback || null;
1011
this.fieldsToExtract = fieldsToExtract instanceof Array ? fieldsToExtract : [fieldsToExtract];
1112
}
1213

1314
ExtractFields(request) {
15+
if (this.customExtractionCallback) {
16+
return this.customExtractionCallback(request);
17+
}
18+
1419
const container = this._getContainer(request);
1520
if (!container) {
16-
return {};
21+
return null;
1722
}
1823
const fields = {};
1924
this.fieldsToExtract.forEach((desiredField) => {
20-
let value = accessNestedObjectValueByStringPath(desiredField.oldFieldName, container) || container[desiredField.oldFieldName];
25+
const value = accessNestedObjectValueByStringPath(desiredField.oldFieldName, container) || container[desiredField.oldFieldName];
2126
if (!value || typeof value !== 'string') {
2227
return;
2328
}
24-
value = desiredField.shouldNormalize ? this._normalizeField(value) : value;
2529
fields[desiredField.newFieldName] = value;
2630
});
2731
return fields;
2832
}
2933

30-
_normalizeField(fieldValue) {
31-
return fieldValue.toLowerCase();
32-
}
33-
3434
_getContainerName(sentThrough) {
3535
switch (sentThrough) {
3636
case SentThrough.QUERY_PARAM:
Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
11
const FieldExtractor = require('./FieldExtractor');
22
const ExtractionField = require('../models/ExtractionField');
3-
const { sha256 } = require('../pxutil');
3+
const { CredentialsIntelligenceProtocolFactory } = require('./credentials_intelligence/CredentialsIntelligenceProtocolFactory');
4+
const { CI_USERNAME_FIELD, CI_PASSWORD_FIELD } = require('../utils/constants');
45

5-
const USERNAME_FIELD = 'user';
6-
const PASSWORD_FIELD = 'pass';
7-
const CREDENTIALS_FIELD = 'creds';
8-
9-
class FieldExtractorManager {
10-
constructor(logger, extractObjects) {
6+
class LoginCredentialsExtractor {
7+
constructor(logger, version, extractObjects) {
118
this.logger = logger;
129
this.extractorMap = this._createExtractorsMap(extractObjects);
10+
this.protocol = CredentialsIntelligenceProtocolFactory.Create(version);
1311
}
1412

15-
ExtractFields(request) {
13+
ExtractLoginCredentials(request) {
1614
const key = this._generateMapKey(request.path, request.method);
1715
const extractor = this.extractorMap[key];
1816
if (!extractor) {
19-
return {};
17+
return null;
2018
}
2119
this.logger.debug(`Attempting to extract credentials for ${request.method} ${request.path} request`);
2220
try {
2321
const fields = extractor.ExtractFields(request);
2422
return this._processFields(fields);
2523
} catch (e) {
26-
this.logger.error(e);
27-
return {};
24+
this.logger.debug(`Encountered error extracting credentials: ${e}`);
25+
return null;
2826
}
2927
}
3028

@@ -36,25 +34,22 @@ class FieldExtractorManager {
3634
const map = {};
3735
for (const extractFields of extractObjects) {
3836
const key = this._generateMapKey(extractFields.path, extractFields.method.toUpperCase());
39-
map[key] = new FieldExtractor(extractFields.sent_through, [
40-
new ExtractionField(extractFields.user_field, USERNAME_FIELD, true),
41-
new ExtractionField(extractFields.pass_field, PASSWORD_FIELD, false)
37+
map[key] = new FieldExtractor(extractFields.sent_through, extractFields.callback, [
38+
new ExtractionField(extractFields.user_field, CI_USERNAME_FIELD),
39+
new ExtractionField(extractFields.pass_field, CI_PASSWORD_FIELD)
4240
]);
4341
}
4442
return map;
4543
}
4644

4745
_processFields(fields) {
48-
if (!fields || !fields[USERNAME_FIELD] || !fields[PASSWORD_FIELD]) {
46+
if (!fields || !fields[CI_USERNAME_FIELD] && !fields[CI_PASSWORD_FIELD]) {
4947
this.logger.debug('Failed extracting credentials');
50-
return {};
48+
return null;
5149
}
52-
this.logger.debug('Successfully extracted credentials')
53-
return {
54-
[USERNAME_FIELD]: sha256(fields[USERNAME_FIELD]),
55-
[CREDENTIALS_FIELD]: sha256(fields[USERNAME_FIELD] + fields[PASSWORD_FIELD])
56-
};
50+
this.logger.debug('Successfully extracted credentials');
51+
return this.protocol.ProcessCredentials(fields[CI_USERNAME_FIELD], fields[CI_PASSWORD_FIELD]);
5752
}
5853
}
5954

60-
module.exports = FieldExtractorManager;
55+
module.exports = LoginCredentialsExtractor;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const { CIVersion } = require('../../enums/CIVersion');
2+
const { V1CredentialsIntelligenceProtocol } = require('./V1CredentialsIntelligenceProtocol');
3+
const { MultistepSSOCredentialsIntelligenceProtocol } = require('./MultistepSSOCredentialsIntelligenceProtocol');
4+
5+
class CredentialsIntelligenceProtocolFactory {
6+
static Create(protocolVersion) {
7+
switch (protocolVersion) {
8+
case CIVersion.V1:
9+
return new V1CredentialsIntelligenceProtocol();
10+
case CIVersion.MULTISTEP_SSO:
11+
return new MultistepSSOCredentialsIntelligenceProtocol();
12+
default:
13+
throw new Error(`Unknown CI protocol version '${protocolVersion}', acceptable versions are ${Object.values(CIVersion)}`);
14+
}
15+
}
16+
}
17+
18+
module.exports = {
19+
CredentialsIntelligenceProtocolFactory
20+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class ICredentialsIntelligenceProtocol {
2+
ProcessCredentials(username, password) {
3+
throw new Error('Unimplemented method!');
4+
}
5+
}
6+
7+
module.exports = {
8+
ICredentialsIntelligenceProtocol
9+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const { ICredentialsIntelligenceProtocol } = require('./ICredentialsIntelligenceProtocol');
2+
const { LoginCredentialsFields } = require('../../models/LoginCredentialsFields');
3+
const { CIVersion } = require('../../enums/CIVersion');
4+
const { sha256 } = require('../../pxutil');
5+
6+
class MultistepSSOCredentialsIntelligenceProtocol extends ICredentialsIntelligenceProtocol {
7+
ProcessCredentials(username, password) {
8+
const rawUsername = username || null;
9+
10+
return new LoginCredentialsFields(
11+
rawUsername,
12+
password ? sha256(password) : null,
13+
rawUsername,
14+
CIVersion.MULTISTEP_SSO
15+
);
16+
}
17+
}
18+
19+
module.exports = {
20+
MultistepSSOCredentialsIntelligenceProtocol
21+
};

0 commit comments

Comments
 (0)