purpleteam
Advanced tools
Comparing version 1.0.0-alpha.3 to 2.0.0-alpha.3
@@ -15,3 +15,3 @@ { | ||
"protocol": "https", | ||
"host": "api.purpleteam-labs.com", | ||
"host": "api[release-environment].purpleteam-labs.com", | ||
"port": 443, | ||
@@ -24,3 +24,3 @@ "stage": "alpha", | ||
"protocol": "https", | ||
"host": "auth.purpleteam-labs.com", | ||
"host": "auth[release-environment].purpleteam-labs.com", | ||
"appClientId": "<your-app-client-id>", | ||
@@ -27,0 +27,0 @@ "appClientSecret": "<your-app-client-secret>", |
@@ -185,4 +185,4 @@ // Copyright (C) 2017-2021 BinaryMist Limited. All rights reserved. | ||
doc: 'The version of the Job accepted by the PurpleTeam 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' | ||
} | ||
@@ -189,0 +189,0 @@ }, |
@@ -6,3 +6,3 @@ { | ||
}, | ||
"version": "1.0.0-alpha.3", | ||
"version": "2.0.0-alpha.3", | ||
"description": "CLI for driving purpleteam -- security regression testing SaaS", | ||
@@ -9,0 +9,0 @@ "main": "src/index.js", |
@@ -24,4 +24,4 @@ <div align="center"> | ||
<br/><br/> | ||
<a href="https://purpleteam-labs.com" title="purpleteam"> | ||
<img width=900px src="https://user-images.githubusercontent.com/2862029/104117134-a93b7780-5383-11eb-8270-bfc46f310a24.png" alt="purpleteam test run"> | ||
<a href="https://www.youtube.com/watch?v=nJNAbGLCGNY" title="purpleteam"> | ||
<img width=900px src="https://user-images.githubusercontent.com/2862029/134789647-515d18db-6d5c-4704-864c-2ee5bcc1d015.png" alt="Full System Run"> | ||
</a> | ||
@@ -204,4 +204,4 @@ | ||
The [_Job_](https://purpleteam-labs.com/doc/definitions/) file is what purpleteam uses to do the following. Most properties should be self documenting. If you are unsure of any, start a [Github discussion](https://github.com/purpleteam-labs/purpleteam/discussions) or reach out in the [#project-purpleteam channel of OWASP Slack](https://owasp.slack.com/messages/project-purpleteam). | ||
Examples of _Job_ files that the purpleteam-labs team uses can be found [here](https://github.com/purpleteam-labs/purpleteam/tree/main/testResources/jobs): | ||
The [_Job_](https://purpleteam-labs.com/doc/definitions/) file is what purpleteam uses to do the following. Most properties should be self documenting, although the official documentation is [here](https://purpleteam-labs.com/doc/job-file/). If you are unsure of any of the properties, start a [Github discussion](https://github.com/purpleteam-labs/purpleteam/discussions) or reach out in the [#project-purpleteam channel of OWASP Slack](https://owasp.slack.com/messages/project-purpleteam). | ||
Examples of _Job_ files that the PurpleTeam-Labs team uses can be found [here](https://github.com/purpleteam-labs/purpleteam/tree/main/testResources/jobs). You will need to define the following: | ||
@@ -273,3 +273,3 @@ * Authenticate to your [System Under Test (_SUT_)](https://purpleteam-labs.com/doc/definitions/) | ||
* `status`: Writes to the console using the purpleteam-logger configured with the `SignaleTransport`, via blessed | ||
* `test`: Writes to file using purpleteam-logger configured with the `File` transport. Writes to the console using blessed. On a successful test run, an outcomes zip file will be written to the directory specified by `outcomes.dir` | ||
* `test`: Writes to file using purpleteam-logger configured with the `File` transport, writes to the console using blessed. On a successful test run, an outcomes zip file will be written to the directory specified by `outcomes.dir` | ||
* `testplan`: Writes to the console using blessed | ||
@@ -279,8 +279,21 @@ * `noUi`: Is well suited to running the _PurpleTeam_ CLI from another process (your build/CI/CD process for example). | ||
* `about`: Writes to the console using the purpleteam-logger configured with the `SignaleTransport`. The about screen is written. Exits with code: "0" | ||
* `status`: Writes to the console using the purpleteam-logger configured with the `SignaleTransport`. `orchestrator is down`... or `orchestrator is up` is written. Exits with code: "0" | ||
* `test`: Writes to file using purpleteam-logger configured with the `File` transport | ||
* If the orchestrator/API is down `orchestrator is down`... is written using the `SignaleTransport`. Exits with code: "0" | ||
* If the orchestrator/API is up, CLI logs will be written to the directory specified by `loggers.testerProgress.dirname` as the Test Run progresses and an outcomes zip file will be written to the directory specified by `outcomes.dir` on Test Run completion. The CLI does not terminate. If the _SUT_ is not found, it will be obvious in the CLI logs | ||
* `status`: Writes the following messages to the console using the purpleteam-logger configured with the `SignaleTransport`. **These messages and their meanings apply to both `uI` modes**: | ||
* `orchestrator is down, or an incorrect URL has been specified in the CLI config` if the _orchestrator_ is unreachable | ||
* `orchestrator is ready to take orders.` | ||
* `Test Run is in progress.` | ||
Exits with code: "0" | ||
* `test`: Writes to file using purpleteam-logger configured with the `File` transport. **These messages and their meanings apply to both `uI` modes** | ||
* If the orchestrator/API is unreachable: `orchestrator is down, or an incorrect URL has been specified in the CLI config` is written using the `SignaleTransport`. Exits with code: "0" | ||
* If the orchestrator/API is reachable CLI logs will be written to the directory specified by `loggers.testerProgress.dirname` as the Test Run progresses and an outcomes zip file will be written to the directory specified by `outcomes.dir` on Test Run completion. The CLI does not terminate | ||
* Retry sequence documented in [this blog post](https://binarymist.io/blog/2021/09/07/purpleteam-tls-tester-implementation/#synchronisation) | ||
* If there is a _Tester_ failure `Tester failure:`... will be written using the `SignaleTransport` and to the directory specified by `loggers.testerProgress.dirname` for the specific _Tester_ that issued the `Tester failure:`... message, so you may want to keep watch on the logs for all _Testers_ if you are searching for the `Tester failure:` string. The _orchestrator_ will issue warning messages for the other _Testers_, but they may not contain the text: `Tester failure:`. | ||
This can happen for varius reasons such as: | ||
* The number of _Test Sessions_ provided in the _Job_ file falls outside of the valid range: | ||
`Tester failure: The only valid number of tlsScanner resource objects is one. Please modify your Job file.` | ||
`Tester failure: The only valid number of appScanner resource objects is from 1-12 inclusive. Please modify your Job file.` | ||
* `Tester failure: S2 app containers were not ready. app Tester(s) failed initialisation. Test Run aborted` - This occurres in the `cloud` environment if ECS doesn't bring the stage two containers up in time. The App Tester gives ECS 2 minutes to bring the stage two containers up, usually they come up from cold start with 40 seconds to spare, if they don't come up in 2 minutes then the App _Tester_ decides it is unable to start a _Test Run_ due to circumstances outside of it's control (ECS is not going to bring the stage two containers up) and the _orchestrator_ aborts the _Test Run_ with this message. The _orchestrator_ then issues the order to bring all stage two containers down (clean-up). | ||
As the _Build User_ you can rely on the text `Tester failure:` to mean you will need to initiate a retry. You can do this after some time, or continue to issue the CLI `status` command, after aproximatly 50 seconds the response will change from `Test Run is in progress.` to `orchestrator is ready to take orders.`, at which point you can initiate a retry (run the `test` command again) | ||
* `testplan`: Writes to file using purpleteam-logger configured with the `File` transport | ||
* If the orchestrator/API is down `orchestrator is down`... is written using the `SignaleTransport`. Exits with code: "0" | ||
* If the orchestrator/API is down `orchestrator is down, or an incorrect URL has been specified in the CLI config` is written using the `SignaleTransport`. Exits with code: "0" | ||
* If the orchestrator/API is up, CLI logs will be written to the directory specified by `loggers.testPlan.dirname` on completion. Exits with code: "0" | ||
@@ -287,0 +300,0 @@ |
@@ -20,5 +20,5 @@ // Copyright (C) 2017-2021 BinaryMist Limited. All rights reserved. | ||
exports.flags = 'status'; | ||
exports.desc = 'Check whether the purpleteam orchestrator is up.'; | ||
exports.desc = 'Check the status of the PurpleTeam back-end.'; | ||
exports.run = async () => { | ||
await api.status(); | ||
}; |
@@ -32,3 +32,2 @@ // Copyright (C) 2017-2021 BinaryMist Limited. All rights reserved. | ||
let job; | ||
const events = { testerProgress: [], testerPctComplete: [], testerBugCount: [] }; | ||
@@ -41,3 +40,3 @@ | ||
const validatedJobFileContent = validateJob(jobFileContents); | ||
job = Bourne.parse(validatedJobFileContent); | ||
this.job = Bourne.parse(validatedJobFileContent); | ||
this.eventNames.forEach((e) => this.initTesterMessages(e)); | ||
@@ -53,3 +52,3 @@ } | ||
initTesterMessages(eventName) { | ||
const appScannerResourceObjectsFromJob = job.included.filter((resourceObj) => resourceObj.type === 'appScanner'); | ||
const appScannerResourceObjectsFromJob = this.job.included.filter((resourceObj) => resourceObj.type === 'appScanner'); | ||
const appScannerResourceObjects = appScannerResourceObjectsFromJob.length ? appScannerResourceObjectsFromJob : [{ id: 'NA' }]; // If Build User supplied no appScanner resource object. | ||
@@ -84,5 +83,5 @@ events[eventName] = appScannerResourceObjects.map((aSRO) => ({ testerType: 'app', sessionId: aSRO.id, messages: [] })); | ||
const appScannerResourceObjects = job.included.filter((resourceObj) => resourceObj.type === 'appScanner'); | ||
const serverScannerResourceObjects = job.included.filter((resourceObj) => resourceObj.type === 'serverScanner'); | ||
const tlsScannerResourceObjects = job.included.filter((resourceObj) => resourceObj.type === 'tlsScanner'); | ||
const appScannerResourceObjects = this.job.included.filter((resourceObj) => resourceObj.type === 'appScanner'); | ||
const serverScannerResourceObjects = this.job.included.filter((resourceObj) => resourceObj.type === 'serverScanner'); | ||
const tlsScannerResourceObjects = this.job.included.filter((resourceObj) => resourceObj.type === 'tlsScanner'); | ||
@@ -89,0 +88,0 @@ testerSessions.push(...(appScannerResourceObjects.length ? appScannerResourceObjects.map((tSRO) => ( |
@@ -239,4 +239,4 @@ // Copyright (C) 2017-2021 BinaryMist Limited. All rights reserved. | ||
} | ||
}, | ||
local: {} | ||
} | ||
// If there was a need for local retry, do the same thing as above, we've tested this. Bear in mind it introduces complexity and possible eadge cases. | ||
}[env]))(); | ||
@@ -305,3 +305,3 @@ | ||
const subscribeToTesterFeedback = (model, testerStatuses) => { | ||
const subscribeToTesterFeedback = (model, testerStatuses, subscribeToOngoingFeedback) => { | ||
const { testerNamesAndSessions } = model; | ||
@@ -321,3 +321,3 @@ testerNamesAndSessions.forEach((testerNameAndSession) => { | ||
}); | ||
if (testerRepresentative.message !== TesterUnavailable(testerNameAndSession.testerType)) { | ||
if (subscribeToOngoingFeedback && testerRepresentative.message !== TesterUnavailable(testerNameAndSession.testerType)) { | ||
const eventSource = new EventSource(`${apiUrl}/${TesterFeedbackRoutePrefix('sse')}/${testerNameAndSession.testerType}/${testerNameAndSession.sessionId}`); // sessionId is 'NA' for tls? | ||
@@ -390,3 +390,3 @@ const handleServerSentTesterEventsClosure = (event) => { | ||
const longPollTesterFeedback = async (model, testerStatuses) => { | ||
const longPollTesterFeedback = async (model, testerStatuses, subscribeToOngoingFeedback) => { | ||
const { testerNamesAndSessions } = model; | ||
@@ -406,3 +406,3 @@ await Promise.all(testerNamesAndSessions.map(async (testerNameAndSession) => { | ||
}); | ||
if (testerRepresentative.message !== TesterUnavailable(testerNameAndSession.testerType)) { | ||
if (subscribeToOngoingFeedback && testerRepresentative.message !== TesterUnavailable(testerNameAndSession.testerType)) { | ||
// If Long Polling via recursion becomes a problem due to: memory usage or stack size, we could: | ||
@@ -450,7 +450,7 @@ // 1. Move requestPollTesterFeedback to me module scoped in this file and call it via setTimeout | ||
const getTesterFeedback = { | ||
sse: async (model, testerStatuses) => { | ||
subscribeToTesterFeedback(model, testerStatuses); | ||
sse: async (model, testerStatuses, subscribeToOngoingFeedback) => { | ||
subscribeToTesterFeedback(model, testerStatuses, subscribeToOngoingFeedback); | ||
}, | ||
lp: async (model, testerStatuses) => { | ||
await longPollTesterFeedback(model, testerStatuses); | ||
lp: async (model, testerStatuses, subscribeToOngoingFeedback) => { | ||
await longPollTesterFeedback(model, testerStatuses, subscribeToOngoingFeedback); | ||
} | ||
@@ -477,4 +477,5 @@ }; | ||
const testPlans = async (jobFileContents) => { | ||
if (!getInitialisedModel(jobFileContents)) return; | ||
const resultingTestPlans = await requestTestPlan(jobFileContents); | ||
const model = getInitialisedModel(jobFileContents); | ||
if (!model) return; | ||
const resultingTestPlans = await requestTestPlan(JSON.stringify(model.job, null, 2)); | ||
resultingTestPlans && view.testPlan({ testPlans: resultingTestPlans, ptLogger }); | ||
@@ -490,5 +491,9 @@ }; | ||
if (!model) return; | ||
const { testerStatuses, testerFeedbackCommsMedium } = await requestTest(jobFileContents); | ||
if (testerStatuses) { | ||
const result = await requestTest(JSON.stringify(model.job, null, 2)); | ||
let testerStatuses; | ||
let testerFeedbackCommsMedium; | ||
if (result) { | ||
({ testerStatuses, testerFeedbackCommsMedium } = result); | ||
view.test(model.testerSessions()); | ||
@@ -499,4 +504,6 @@ model.eventNames.forEach((eN) => { | ||
await getTesterFeedback[testerFeedbackCommsMedium](model, testerStatuses); | ||
const subscribeToOngoingFeedback = !testerStatuses.find((e) => e.message.startsWith('Tester failure:')); | ||
await getTesterFeedback[testerFeedbackCommsMedium](model, testerStatuses, subscribeToOngoingFeedback); | ||
// To cancel the event stream: | ||
@@ -503,0 +510,0 @@ // https://github.com/mtharrison/susie#how-do-i-finish-a-sse-stream-for-good |
@@ -29,3 +29,7 @@ // Copyright (C) 2017-2021 BinaryMist Limited. All rights reserved. | ||
const aPiSchema = require('./job.aPi'); | ||
const browserAppSchema = require('./job.browserApp'); | ||
const internals = { | ||
recognisedJobTypes: ['Api', 'BrowserApp'], | ||
config: { | ||
@@ -36,228 +40,6 @@ sut: null, | ||
log: null, | ||
validate: null | ||
validateApi: null, | ||
validateBrowserApp: null | ||
}; | ||
// Used quicktype to generate initial schema from job | ||
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: ['job'] }, | ||
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 } | ||
}, | ||
required: [ | ||
'browser', | ||
'loggedInIndicator', | ||
'sutAuthentication', | ||
'sutIp', | ||
'sutPort', | ||
'sutProtocol', | ||
'version' | ||
], | ||
title: 'DataAttributes', | ||
errorMessage: { properties: { loggedInIndicator: 'A loggedInIndicator is required by the App emissary in order to know if a login was successful' } } | ||
}, | ||
SutAuthentication: { | ||
type: 'object', | ||
additionalProperties: false, | ||
properties: { | ||
route: { type: 'string', pattern: '^/\\w{1,200}$' }, | ||
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: [ | ||
'passwordFieldLocater', | ||
'route', | ||
'submit', | ||
'usernameFieldLocater', | ||
'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{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{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}$' }, | ||
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 } | ||
}, | ||
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: { type: 'string' }, | ||
visible: { type: 'boolean' } // Todo: KC: Need to check whether visible should be required. | ||
}, | ||
required: [ | ||
'name', | ||
'value' | ||
], | ||
title: 'AttackField' | ||
} | ||
} | ||
}; | ||
const convertJsonToObj = (value) => ((typeof value === 'string' || value instanceof String) ? Bourne.parse(value) : value); | ||
@@ -279,6 +61,6 @@ const deltaLogs = (initialConfig, possiblyMutatedConfig) => { | ||
const validateJob = (jobString) => { | ||
const { validate } = internals; | ||
const job = convertJsonToObj(jobString); | ||
const validate = internals[`validate${job.data.type}`]; | ||
if (!validate) throw new Error(`The Job type supplied is incorrect, please choose one of ${JSON.stringify(internals.recognisedJobTypes, null, 2)}.`); | ||
// Todo: Kim C: Will need to test various configs. | ||
if (!validate(job)) { | ||
@@ -300,3 +82,9 @@ const validationError = new Error(JSON.stringify(validate.errors, null, 2)); | ||
internals.log = purpleteamLogger.init(loggerConfig); | ||
internals.validate = ajv.compile(schema); | ||
aPiSchema.init(internals.config); | ||
browserAppSchema.init(internals.config); | ||
internals.validateApi = ajv.compile(aPiSchema.schema); | ||
internals.validateBrowserApp = ajv.compile(browserAppSchema.schema); | ||
return { validateJob }; | ||
@@ -303,0 +91,0 @@ }; |
@@ -17,4 +17,4 @@ // Copyright (C) 2017-2021 BinaryMist Limited. All rights reserved. | ||
const TesterUnavailable = (tester) => `No ${tester} testing available currently. The ${tester} tester is currently in-active.`; | ||
const TestPlanUnavailable = (tester) => `No test plan available for the ${tester} tester. The ${tester} tester is currently in-active.`; | ||
const TesterUnavailable = (tester) => `No ${tester} testing available currently. The ${tester} Tester is currently in-active.`; // Should match orchestrator. | ||
const TestPlanUnavailable = (tester) => `No test plan available for the ${tester} Tester. The ${tester} Tester is currently in-active.`; // Should match orchestrator. | ||
@@ -21,0 +21,0 @@ const TesterFeedbackRoutePrefix = (m) => ({ sse: 'tester-feedback', lp: 'poll-tester-feedback' }[m]); |
@@ -78,3 +78,3 @@ // Copyright (C) 2017-2021 BinaryMist Limited. All rights reserved. | ||
args: { | ||
label: 'New Bugs', | ||
label: 'New Alerts', | ||
segmentWidth: 0.06, | ||
@@ -81,0 +81,0 @@ segmentInterval: 0.1, |
162717
27
2094
516