Socket
Socket
Sign inDemoInstall

purpleteam

Package Overview
Dependencies
194
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.0.0-alpha.3 to 2.0.0-alpha.3

src/schemas/job.aPi.js

4

config/config.example.cloud.json

@@ -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,

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc