Skip to content

Commit 0e4d4ff

Browse files
committed
Add API scanning
Add support for OpenApi, GraphQl, SOAP import URL APIs. Add authentication support for JWT strategy. Add API support to Emissary (zap). Create Cucumber feature file and steps. Move existing app feature file and steps. Split Cucumber world into BrowserApp and Api. Moved percentEncode from browser to strings as Api SUTs also need it.
1 parent 940db18 commit 0e4d4ff

File tree

37 files changed

+1521
-232
lines changed

37 files changed

+1521
-232
lines changed

config/config.example.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
}
1616
},
1717
"cucumber": {
18-
"tagExpression": "@app_scan",
1918
"timeout": 1800000
2019
},
2120
"results": {

config/config.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,6 @@ const schema = {
219219
format: String,
220220
default: 'src/steps'
221221
},
222-
tagExpression: {
223-
doc: 'The tag expression without the \'--tag\' to run Cucumber with.',
224-
format: String,
225-
default: 'not @simple_math'
226-
},
227222
binary: {
228223
doc: 'The location of the Cucumber binary.',
229224
format: String,

src/api/app/do/sUt.aPi.js

Lines changed: 96 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,61 @@ class Api extends Sut {
2929
// ...
3030

3131
#createSchema() {
32-
this.#sutSchema = Joi.object({ });
32+
this.#sutSchema = Joi.object({
33+
sUtType: Joi.string().required().valid('Api'),
34+
protocol: Joi.string().required().valid('https', 'http'),
35+
ip: Joi.string().hostname().required(),
36+
port: Joi.number().port().required(),
37+
// eslint-disable-next-line no-underscore-dangle
38+
browser: Joi.string().valid(...this.#configSchemaProps.sut._cvtProperties.browser.format).lowercase().default(this.config.get('sut.browser')), // Todo: Remove once selenium containers are removed.
39+
loggedInIndicator: Joi.string(),
40+
loggedOutIndicator: Joi.string(),
41+
context: Joi.object({ // Zap context
42+
id: Joi.number().integer().positive(), // Provided by Zap.
43+
name: Joi.string().token() // Created in the app.js model.
44+
}),
45+
userId: Joi.number().integer().positive(), // Provided by Zap.
46+
authentication: Joi.object({
47+
emissaryAuthenticationStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('MaintainJwt'),
48+
route: Joi.string().min(2).regex(/^\/[-?&=\w/]{1,1000}$/)
49+
}),
50+
testSession: Joi.object({
51+
type: Joi.string().valid('appScanner').required(),
52+
id: Joi.string().alphanum().required(),
53+
attributes: Joi.object({
54+
sitesTreePopulationStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('ImportUrls'),
55+
spiderStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('Standard'),
56+
scannersStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('ApiStandard'),
57+
scanningStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('ApiStandard'),
58+
postScanningStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('ApiStandard'),
59+
reportingStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('Standard'),
60+
username: Joi.string().min(2).required(),
61+
openApi: Joi.object({
62+
importFileContentBase64: Joi.string().base64({ paddingRequired: true }),
63+
importUrl: Joi.string().uri({ scheme: ['https', 'http'], domain: { allowUnicode: false } })
64+
}).xor('importFileContentBase64', 'importUrl'),
65+
soap: Joi.object({
66+
importFileContentBase64: Joi.string().base64({ paddingRequired: true }),
67+
importUrl: Joi.string().uri({ scheme: ['https', 'http'], domain: { allowUnicode: false } })
68+
}).xor('importFileContentBase64', 'importUrl'),
69+
graphQl: Joi.object({
70+
importFileContentBase64: Joi.string().base64({ paddingRequired: true }),
71+
importUrl: Joi.string().uri({ scheme: ['https', 'http'], domain: { allowUnicode: false } }),
72+
maxQueryDepth: Joi.number().integer().positive(), // Zaproxy default: 5
73+
maxArgsDepth: Joi.number().integer().positive(), // Zaproxy default: 5
74+
optionalArgsEnabled: Joi.boolean().default(true), // Zaproxy default: true
75+
argsType: Joi.string().valid('INLINE', 'VARIABLES', 'BOTH'), // Zaproxy default: 'BOTH'
76+
querySplitType: Joi.string().valid('LEAF', 'ROOT_FIELD', 'OPERATION'), // Zaproxy default: 'LEAF'
77+
requestMethod: Joi.string().valid('POST_JSON', 'POST_GRAPHQL', 'GET') // Zaproxy default: 'POST_JSON'
78+
}).xor('importFileContentBase64', 'importUrl'),
79+
importUrls: Joi.object({ importFileContentBase64: Joi.string().base64({ paddingRequired: true }).required() }),
80+
aScannerAttackStrength: Joi.string().valid(...this.#configSchemaProps.sut._cvtProperties.aScannerAttackStrength.format).uppercase().default(this.config.get('sut.aScannerAttackStrength')), // eslint-disable-line no-underscore-dangle
81+
aScannerAlertThreshold: Joi.string().valid(...this.#configSchemaProps.sut._cvtProperties.aScannerAlertThreshold.format).uppercase().default(this.config.get('sut.aScannerAlertThreshold')), // eslint-disable-line no-underscore-dangle
82+
alertThreshold: Joi.number().integer().min(0).max(1000).default(this.config.get('sut.alertThreshold')),
83+
excludedRoutes: Joi.array().items(Joi.string()).default([])
84+
}).xor('openApi', 'graphQl', 'soap', 'importUrls')
85+
})
86+
}).xor('loggedInIndicator', 'loggedOutIndicator');
3387
}
3488

3589
async #selectStrategies() {
@@ -38,8 +92,6 @@ class Api extends Sut {
3892

3993
async initialise() { // eslint-disable-line class-methods-use-this
4094
// Todo: Populate as required.
41-
42-
4395
}
4496

4597
constructor({ log, publisher, sutProperties }) {
@@ -57,49 +109,83 @@ class Api extends Sut {
57109
getSitesTreePopulationStrategy() {
58110
return {
59111
...super.getSitesTreePopulationStrategy(),
60-
args: { /* Todo: args specific to the API specific strategy */ }
112+
args: {
113+
log: this.log,
114+
publisher: this.publisher,
115+
baseUrl: this.baseUrl(),
116+
sutPropertiesSubSet: this.getProperties(['testSession', 'context']),
117+
setContextId: (id) => { this.properties.context.id = id; }
118+
}
61119
};
62120
}
63121

64122
getEmissaryAuthenticationStrategy() {
65123
return {
66124
...super.getEmissaryAuthenticationStrategy(),
67-
args: { /* Todo: args specific to the API specific strategy */ }
125+
args: {
126+
log: this.log,
127+
publisher: this.publisher,
128+
baseUrl: this.baseUrl(),
129+
sutPropertiesSubSet: this.getProperties(['authentication', 'loggedInIndicator', 'loggedOutIndicator', 'testSession', 'context']),
130+
setUserId: (id) => { this.properties.userId = id; }
131+
}
68132
};
69133
}
70134

71135
getSpiderStrategy() {
72136
return {
73137
...super.getSpiderStrategy(),
74-
args: { /* Todo: args specific to the API specific strategy */ }
138+
args: {
139+
publisher: this.publisher,
140+
baseUrl: this.baseUrl(),
141+
sutPropertiesSubSet: this.getProperties('testSession')
142+
}
75143
};
76144
}
77145

78146
getScannersStrategy() {
79147
return {
80148
...super.getScannersStrategy(),
81-
args: { /* Todo: args specific to the API specific strategy */ }
149+
args: {
150+
log: this.log,
151+
publisher: this.publisher,
152+
baseUrl: this.baseUrl(),
153+
sutPropertiesSubSet: this.getProperties('testSession')
154+
}
82155
};
83156
}
84157

85158
getScanningStrategy() {
86159
return {
87160
...super.getScanningStrategy(),
88-
args: { /* Todo: args specific to the API specific strategy */ }
161+
args: {
162+
log: this.log,
163+
publisher: this.publisher,
164+
baseUrl: this.baseUrl(),
165+
sutPropertiesSubSet: this.getProperties(['testSession', 'context', 'userId'])
166+
}
89167
};
90168
}
91169

92170
getPostScanningStrategy() {
93171
return {
94172
...super.getPostScanningStrategy(),
95-
args: { /* Todo: args specific to the API specific strategy */ }
173+
args: {
174+
publisher: this.publisher,
175+
baseUrl: this.baseUrl(),
176+
sutPropertiesSubSet: this.getProperties('testSession')
177+
}
96178
};
97179
}
98180

99181
getReportingStrategy() {
100182
return {
101183
...super.getReportingStrategy(),
102-
args: { /* Todo: args specific to the API specific strategy */ }
184+
args: {
185+
log: this.log,
186+
publisher: this.publisher,
187+
sutPropertiesSubSet: this.getProperties('testSession')
188+
}
103189
};
104190
}
105191
}

src/api/app/do/sUt.browserApp.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class BrowserApp extends Sut {
3232

3333
#createSchema() {
3434
this.#sutSchema = Joi.object({
35-
sUtType: Joi.string().required().valid('BrowserApp', 'Api'),
35+
sUtType: Joi.string().required().valid('BrowserApp'),
3636
protocol: Joi.string().required().valid('https', 'http'),
3737
ip: Joi.string().hostname().required(),
3838
port: Joi.number().port().required(),
@@ -58,12 +58,12 @@ class BrowserApp extends Sut {
5858
id: Joi.string().alphanum().required(),
5959
attributes: Joi.object({
6060
sitesTreePopulationStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('WebDriverStandard'),
61-
spiderStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('BrowserAppStandard'),
61+
spiderStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('Standard'),
6262
scannersStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('BrowserAppStandard'),
63-
scanningStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('Standard'),
64-
postScanningStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('Standard'),
63+
scanningStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('BrowserAppStandard'),
64+
postScanningStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('BrowserAppStandard'),
6565
reportingStrategy: Joi.string().min(2).regex(/^[-\w/]{1,200}$/).default('Standard'),
66-
username: Joi.string().min(2),
66+
username: Joi.string().min(2).required(),
6767
password: Joi.string().min(2),
6868
aScannerAttackStrength: Joi.string().valid(...this.#configSchemaProps.sut._cvtProperties.aScannerAttackStrength.format).uppercase().default(this.config.get('sut.aScannerAttackStrength')), // eslint-disable-line no-underscore-dangle
6969
aScannerAlertThreshold: Joi.string().valid(...this.#configSchemaProps.sut._cvtProperties.aScannerAlertThreshold.format).uppercase().default(this.config.get('sut.aScannerAlertThreshold')), // eslint-disable-line no-underscore-dangle
@@ -90,7 +90,7 @@ class BrowserApp extends Sut {
9090
submit: Joi.string().min(2).regex(/^[a-z0-9_-]+/i)
9191
})
9292
}))
93-
});
93+
}).xor('loggedInIndicator', 'loggedOutIndicator');
9494
}
9595

9696
#selectStrategies() {
@@ -158,7 +158,8 @@ class BrowserApp extends Sut {
158158
publisher: this.publisher,
159159
baseUrl: this.baseUrl(),
160160
browser,
161-
sutPropertiesSubSet: this.getProperties(['testSession', 'testRoutes'])
161+
sutPropertiesSubSet: this.getProperties(['testSession', 'context', 'testRoutes']),
162+
setContextId: (id) => { this.properties.context.id = id; }
162163
}
163164
};
164165
}
@@ -170,9 +171,7 @@ class BrowserApp extends Sut {
170171
log: this.log,
171172
publisher: this.publisher,
172173
baseUrl: this.baseUrl(),
173-
browser,
174-
sutPropertiesSubSet: this.getProperties(['authentication', 'loggedInIndicator', 'loggedOutIndicator', 'excludeFromContext', 'testSession', 'context']),
175-
setContextId: (id) => { this.properties.context.id = id; },
174+
sutPropertiesSubSet: this.getProperties(['authentication', 'loggedInIndicator', 'loggedOutIndicator', 'testSession', 'context']),
176175
setUserId: (id) => { this.properties.userId = id; }
177176
}
178177
};

src/api/app/models/app.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,12 @@ class App {
8484
const testSessions = testJob.included.filter((resourceObject) => resourceObject.type === 'appScanner');
8585

8686
this.#sessionsProps = testSessions.map((sesh) => ({
87-
testRoutes,
87+
...(testRoutes.length > 0 ? { testRoutes } : {/* No test routes in API schema */}),
8888
sUtType: testJob.data.type,
8989
protocol: testJob.data.attributes.sutProtocol,
9090
ip: testJob.data.attributes.sutIp,
9191
port: testJob.data.attributes.sutPort,
92-
browser: testJob.data.attributes.browser,
92+
browser: testJob.data.attributes.browser || 'chrome', // Todo: Needs removing for API, along with selenium containers.
9393
loggedInIndicator: testJob.data.attributes.loggedInIndicator,
9494
loggedOutIndicator: testJob.data.attributes.loggedOutIndicator,
9595
context: { name: `${sesh.id}_Context` },
@@ -140,8 +140,8 @@ class App {
140140
}
141141

142142

143-
async testPlan(testJob) { // eslint-disable-line no-unused-vars
144-
const cucumberArgs = this.#createCucumberArgs({});
143+
async testPlan(testJob) {
144+
const cucumberArgs = this.#createCucumberArgs({ sessionProps: { sUtType: testJob.data.type } });
145145
const cucumberCliInstance = new cucumber.Cli({
146146
argv: ['node', ...cucumberArgs],
147147
cwd: process.cwd(),
@@ -157,7 +157,7 @@ class App {
157157
}
158158

159159
// Receiving appEmissaryPort and seleniumPort are only essential if running in cloud environment.
160-
#createCucumberArgs({ sessionProps = {}, emissaryHost = this.#emissary.hostname, seleniumContainerName = '', appEmissaryPort = this.#emissary.port, seleniumPort = 4444 }) {
160+
#createCucumberArgs({ sessionProps, emissaryHost = this.#emissary.hostname, seleniumContainerName = '', appEmissaryPort = this.#emissary.port, seleniumPort = 4444 }) {
161161
this.#log.debug(`seleniumContainerName is: ${seleniumContainerName}`, { tags: ['app'] });
162162
const emissaryProperties = {
163163
hostname: emissaryHost,
@@ -184,16 +184,16 @@ class App {
184184

185185
const cucumberArgs = [
186186
this.#cucumber.binary,
187-
this.#cucumber.features,
187+
`${this.#cucumber.features}/${sessionProps.sUtType}`,
188188
'--require',
189-
this.#cucumber.steps,
189+
`${this.#cucumber.steps}/${sessionProps.sUtType}`,
190190
/* '--exit', */
191191
`--format=message:${this.#results.dir}result_appScannerId-${sessionProps.testSession ? sessionProps.testSession.id : 'noSessionPropsAvailable'}_${this.#strings.NowAsFileName('-')}.NDJSON`,
192192
/* Todo: Provide ability for Build User to pass flag to disable colours */
193193
'--format-options',
194194
'{"colorsEnabled": true}',
195195
'--tags',
196-
this.#cucumber.tagExpression,
196+
sessionProps.sUtType === 'BrowserApp' ? '@app_scan' : '@api_scan',
197197
'--world-parameters',
198198
parameters
199199
];

src/clients/browser.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,6 @@ const checkAndNotifyBuildUserIfAnyKnownBrowserErrors = async (testSessionId) =>
129129
}
130130
};
131131

132-
const percentEncode = (str) => str.split('').map((char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`).reduce((accum, cV) => `${accum}${cV}`, '');
133-
134132
module.exports = {
135133
findElementThenClick,
136134
findElementThenClear,
@@ -145,6 +143,5 @@ module.exports = {
145143
},
146144
getWebDriver() {
147145
return internals.driver;
148-
},
149-
percentEncode
146+
}
150147
};

0 commit comments

Comments
 (0)