Skip to content

Commit

Permalink
Support for addition of SUT & Emissary strategies
Browse files Browse the repository at this point in the history
Split schema and validator from single Job into BrowserApp and Api to
support addition of SUT and Emissary strategies.
Update .env.example to include the env var referencing the new
required source directory.
Update compose file to include new bind mount.
Increase local duration before next Test Run can be initiated from
10 to 15 seconds.
  • Loading branch information
binarymist committed Nov 18, 2021
1 parent 5aaec7d commit 6fafa85
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 234 deletions.
4 changes: 4 additions & 0 deletions compose/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
# Orchestrator and tester containers share the same group.
# They also read, write and delete outcomes files within this directory.
HOST_DIR=</mnt/your-spare-drive/Logs/purpleteam/outcomes>
# This directory needs group rwx and other r (for the Emissary to read) permissions.
# Both app-scanner and it's Emissary mount this directory. The app-scanner puts files here for the Emissary to consume.
# This environment variable and the same value is also required by the App Emissary in the purpleteam-s2-containers project.
HOST_DIR_APP_SCANNER=</mnt/your-spare-drive/purpleteam-app-scanner>
3 changes: 3 additions & 0 deletions compose/orchestrator-testers-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ services:
- type: bind
source: ${HOST_DIR}
target: /var/log/purpleteam/outcomes
- type: bind
source: ${HOST_DIR_APP_SCANNER}
target: /mnt/purpleteam-app-scanner

tls-scanner:
depends_on:
Expand Down
6 changes: 3 additions & 3 deletions config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ const schema = {
job: {
version: {
doc: 'The version of the Job accepted by this API.',
format: ['0.1.0-alpha.1', '1.0.0-alpha.3'],
default: '1.0.0-alpha.3'
format: ['0.1.0-alpha.1', '1.0.0-alpha.3', '2.0.0-alpha.3'],
default: '2.0.0-alpha.3'
}
},
outcomes: {
Expand Down Expand Up @@ -174,7 +174,7 @@ const schema = {
timeout: {
doc: 'The duration in milliseconds between Test Runs before another test command can be initiated. Important to make sure cleanup has occurred before starting another Test Run. In the cloud ECS requires a longer cool-down period before restarting S2 Tasks.',
format: 'duration',
default: 10000
default: 15000
}
}
};
Expand Down
1 change: 0 additions & 1 deletion src/api/orchestration/models/orchestrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ class Orchestrate {
return !this.#initTesterResponsesForCli ? 'orchestrator is ready to take orders.' : 'Test Run is in progress.';
}

// Could potentially be exposed as part of the API for CLI to invoke if the back-end gets messed up.
async resetTesters({ level = 'soft' }) {
const { cleanUpAfterTestRun: cleanUpTesterWatcherAfterTestRun } = this.#testerWatcher;
const reset = {
Expand Down
9 changes: 9 additions & 0 deletions src/api/orchestration/schemas/job.aPi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const internals = { config: null };

const init = (config) => {
internals.config = config;
};

const schema = {};

module.exports = { init, schema };
249 changes: 249 additions & 0 deletions src/api/orchestration/schemas/job.browserApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
const internals = { config: null };

const init = (config) => {
internals.config = config;
};

const schema = {
$schema: 'http://json-schema.org/draft-07/schema#',
$ref: '#/definitions/Job',
definitions: {
Job: {
type: 'object',
additionalProperties: false,
properties: {
data: { $ref: '#/definitions/Data' },
included: {
type: 'array',
items: { $ref: '#/definitions/TopLevelResourceObject' }
}
},
required: [
'data',
'included'
],
title: 'Job'
},
Data: {
type: 'object',
additionalProperties: false,
properties: {
type: { type: 'string', enum: ['BrowserApp'] },
attributes: { $ref: '#/definitions/DataAttributes' },
relationships: { $ref: '#/definitions/Relationships' }
},
required: [
'attributes',
'relationships',
'type'
],
title: 'Data'
},
DataAttributes: {
type: 'object',
additionalProperties: false,
properties: {
version: { type: 'string', get const() { return internals.config.job.version; } },
sutAuthentication: { $ref: '#/definitions/SutAuthentication' },
sutIp: { type: 'string', oneOf: [{ format: 'ipv6' }, { format: 'hostname' }] },
sutPort: { type: 'integer', minimum: 1, maximum: 65535 },
sutProtocol: { type: 'string', enum: ['https', 'http'], default: 'https' },
browser: { type: 'string', get enum() { return internals.config.sut.browserOptions; }, get default() { return internals.config.sut.defaultBrowser; } },
loggedInIndicator: { type: 'string', minLength: 1 },
loggedOutIndicator: { type: 'string', minLength: 1 }
},
oneOf: [
{ required: ['loggedInIndicator'] },
{ required: ['loggedOutIndicator'] }
],
required: [
'browser',
'sutAuthentication',
'sutIp',
'sutPort',
'sutProtocol',
'version'
],
title: 'DataAttributes'
},
SutAuthentication: {
type: 'object',
additionalProperties: false,
properties: {
sitesTreeSutAuthenticationPopulationStrategy: { type: 'string', enum: ['FormStandard', 'Link'], default: 'FormStandard' },
emissaryAuthenticationStrategy: { type: 'string', enum: ['FormStandard', 'ScriptLink'], default: 'FormStandard' },
route: { type: 'string', pattern: '^/[-?&=\\w/]{1,1000}$' },
usernameFieldLocater: { type: 'string', pattern: '^[a-zA-Z0-9_. -]{1,100}$' }, // Possibly allow spaces for css selectors.
passwordFieldLocater: { type: 'string', pattern: '^[a-zA-Z0-9_. -]{1,100}$' }, // Possibly allow spaces for css selectors.
submit: { type: 'string', pattern: '^[a-zA-Z0-9_\\-\\s]{1,100}$' },
expectedPageSourceSuccess: { type: 'string', minLength: 2, maxLength: 200 }
},
required: [
'route',
'expectedPageSourceSuccess'
],
title: 'SutAuthentication'
},
Relationships: {
type: 'object',
additionalProperties: false,
properties: {
data: {
type: 'array',
items: { $ref: '#/definitions/ResourceLinkage' }
}
},
required: [
'data'
],
title: 'Relationships'
},
ResourceLinkage: {
type: 'object',
additionalProperties: false,
properties: {
type: { type: 'string', enum: ['tlsScanner', 'appScanner', 'route'] },
id: { type: 'string' }
},
required: ['id', 'type'],
if: { properties: { type: { enum: ['tlsScanner'] } } },
then: { properties: { id: { type: 'string', pattern: 'NA' } } },
else: {
if: { properties: { type: { enum: ['appScanner'] } } },
then: { properties: { id: { type: 'string', pattern: '^\\w[-\\w]{1,200}$' } } },
else: {
if: { properties: { type: { enum: ['route'] } } },
then: { properties: { id: { type: 'string', pattern: '^/[-\\w/]{1,200}$' } } }
}
},
title: 'ResourceLinkage'
},
TopLevelResourceObject: {
type: 'object',
additionalProperties: false,
properties: {
type: { type: 'string', enum: ['tlsScanner', 'appScanner', 'route'] },
id: { type: 'string' },
attributes: {},
relationships: {}
},
required: [
'attributes',
'id',
'type'
],
if: { properties: { type: { enum: ['tlsScanner'] } } },
then: {
properties: {
id: { type: 'string', pattern: 'NA' },
attributes: { $ref: '#/definitions/AttributesObjOfTopLevelResourceObjectOfTypeTlsScanner' }
}
},
// If we want to use flags for regex, etc, then need to use ajv-keywords: https://github.com/epoberezkin/ajv-keywords#regexp
else: {
if: { properties: { type: { enum: ['appScanner'] } } },
then: {
properties: {
id: { type: 'string', pattern: '^\\w[-\\w]{1,200}$' },
attributes: { $ref: '#/definitions/AttributesObjOfTopLevelResourceObjectOfTypeAppScanner' },
relationships: { $ref: '#/definitions/Relationships' }
},
required: ['relationships']
},
else: {
if: { properties: { type: { enum: ['route'] } } },
then: {
properties: {
id: { type: 'string', pattern: '^/[-\\w/]{1,200}$' },
attributes: { $ref: '#/definitions/AttributesObjOfTopLevelResourceObjectOfTypeRoute' }
}
}
}
},
title: 'TopLevelResourceObject',
errorMessage: {
properties: {
type: 'should be one of either tlsScanner, appScanner, or route',
id: 'If type is tlsScanner, the id should be NA. If type is appScanner, the id should be a valid appScanner. If type is route, the id should be a valid route.'
}
}
},

AttributesObjOfTopLevelResourceObjectOfTypeTlsScanner: {
type: 'object',
additionalProperties: false,
properties: {
tlsScannerSeverity: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] },
alertThreshold: { type: 'integer', minimum: 0, maximum: 1000 }
},
required: [],
title: 'AttributesObjOfTopLevelResourceObjectOfTypeTlsScanner'
},

AttributesObjOfTopLevelResourceObjectOfTypeAppScanner: {
type: 'object',
additionalProperties: false,
properties: {
username: { type: 'string', pattern: '^([a-zA-Z0-9_-]{1,100}|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z0-9]{2,})$' }, // https://www.py4u.net/discuss/1646374
password: { type: 'string' },
aScannerAttackStrength: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH', 'INSANE'] },
aScannerAlertThreshold: { type: 'string', enum: ['LOW', 'MEDIUM', 'HIGH'] },
alertThreshold: { type: 'integer', minimum: 0, maximum: 1000 },
sitesTreePopulationStrategy: { type: 'string', enum: ['WebDriverStandard'], default: 'WebDriverStandard' },
spiderStrategy: { type: 'string', enum: ['BrowserAppStandard'], default: 'BrowserAppStandard' },
scannersStrategy: { type: 'string', enum: ['BrowserAppStandard'], default: 'BrowserAppStandard' },
scanningStrategy: { type: 'string', enum: ['Standard'], default: 'Standard' },
postScanningStrategy: { type: 'string', enum: ['Standard'], default: 'Standard' },
reportingStrategy: { type: 'string', enum: ['Standard'], default: 'Standard' },
excludedRoutes: {
type: 'array',
items: { type: 'string', pattern: '^/[-?&=.*\\w/]{1,1000}$' },
uniqueItems: true,
minItems: 0
}
},
required: [],
title: 'AttributesObjOfTopLevelResourceObjectOfTypeAppScanner'
},

AttributesObjOfTopLevelResourceObjectOfTypeRoute: {
type: 'object',
additionalProperties: false,
properties: {
attackFields: {
type: 'array',
items: { $ref: '#/definitions/AttackField' },
uniqueItems: true,
minItems: 0
},
method: { type: 'string', enum: ['GET', 'PUT', 'POST'] },
submit: { type: 'string', pattern: '^[a-zA-Z0-9_\\-\\s]{1,100}$' }
},
required: ['attackFields', 'method', 'submit'],
title: 'AttributesObjOfTopLevelResourceObjectOfTypeRoute'
},

AttackField: {
type: 'object',
additionalProperties: false,
properties: {
name: { type: 'string', pattern: '^[a-zA-Z0-9._\\-]{1,100}$' },
value: {
anyOf: [
{ type: 'string' },
{ type: 'boolean' },
{ type: 'number' }
]
},
visible: { type: 'boolean' } // Todo: KC: Need to check whether visible should be required.
},
required: [
'name',
'value'
],
title: 'AttackField'
}
}
};

module.exports = { init, schema };
Loading

0 comments on commit 6fafa85

Please sign in to comment.