├── __tests__ ├── rateLimitTests.js ├── validateConfig.js ├── adaptiveRateLimitingTest.js ├── maliciousInjectionTests.js ├── fieldDuplicationTest.js ├── depthLimitTests.js └── queryBatchTest.js ├── .gitignore ├── .DS_Store ├── src ├── .DS_Store ├── tests │ ├── fieldDuplicationTest.js │ ├── rateLimitTest.js │ ├── adaptiveRateLimitingTest.js │ ├── queryBatchTest.js │ ├── depthLimitTests.js │ └── maliciousInjectionTests.js └── getSchema.js ├── assets ├── qevlar_logo_black.png ├── qevlar_github-banner.png ├── qevlar_test_menufinal.png ├── qevlar_depth_limit_snippet.png ├── qevlar_rate_limit_snippet.png └── qevlar_sql_injection_snippet.png ├── qevlarConfig.json ├── scripts └── postInstall.js ├── package.json ├── color.js ├── qevlar └── README.md /__tests__/rateLimitTests.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /assets/qevlar_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_logo_black.png -------------------------------------------------------------------------------- /assets/qevlar_github-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_github-banner.png -------------------------------------------------------------------------------- /assets/qevlar_test_menufinal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_test_menufinal.png -------------------------------------------------------------------------------- /assets/qevlar_depth_limit_snippet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_depth_limit_snippet.png -------------------------------------------------------------------------------- /assets/qevlar_rate_limit_snippet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_rate_limit_snippet.png -------------------------------------------------------------------------------- /assets/qevlar_sql_injection_snippet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/qevlar/HEAD/assets/qevlar_sql_injection_snippet.png -------------------------------------------------------------------------------- /qevlarConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ANY_TOP_LEVEL_FIELD_ID": "", 3 | "API_URL": "http://localhost:3001/graphql", 4 | "BATCH_SIZE": 10, 5 | "CIRCULAR_REF_FIELD": "Food", 6 | "INCREMENT": 10, 7 | "INITIAL_RATE": 10, 8 | "NO_SQL": false, 9 | "QUERY_DEPTH_LIMIT": 5, 10 | "QUERY_RATE_LIMIT": 100, 11 | "SQL": false, 12 | "SQL_COLUMN_NAME": "", 13 | "SQL_TABLE_NAME": "", 14 | "SUB_FIELD": "id", 15 | "TIME_WINDOW": 1000, 16 | "TOP_LEVEL_FIELD": "meal" 17 | } -------------------------------------------------------------------------------- /scripts/postInstall.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const scriptName = 'qevlar'; 5 | const scriptCommand = 'node node_modules/qevlar'; 6 | 7 | const packageJsonPath = path.join('../../', 'package.json'); 8 | console.log(packageJsonPath); 9 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath)); 10 | 11 | packageJson.scripts[scriptName] = scriptCommand; 12 | 13 | fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qevlar", 3 | "displayName": "Qevlar", 4 | "version": "1.0.0", 5 | "description": "GraphQL Security Testing Library", 6 | "keywords": [ 7 | "graphql", 8 | "API", 9 | "security", 10 | "testing" 11 | ], 12 | "icon": "assets/qevlar-logo-black.png", 13 | "main": "qevlar", 14 | "scripts": { 15 | "start": "qevlar", 16 | "qevlar": "qevlar", 17 | "test": "jest" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/oslabs-beta/Qevlar" 22 | }, 23 | "author": "Joshua McDaniel jwilliammcdaniel@gmail.com, Conor Bell conorbell27@gmail.com, Hyung Noh johnhyungilnoh@gmail.com, Landon Osteen landonwyatteosteen@gmail.com", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/oslabs-beta/Qevlar/issues" 27 | }, 28 | "dependencies": { 29 | "readme-md-generator": "^1.0.0" 30 | }, 31 | "devDependencies": { 32 | "jest": "^29.7.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /__tests__/validateConfig.js: -------------------------------------------------------------------------------- 1 | const { redBold, highlight } = require('../color'); 2 | 3 | function validateConfig(config) { 4 | const expectedTypes = { 5 | ANY_TOP_LEVEL_FIELD_ID: ['number', 'string'], 6 | API_URL: ['string'], 7 | BATCH_SIZE: ['number'], 8 | CIRCULAR_REF_FIELD: ['string'], 9 | INCREMENT: ['number'], 10 | INITIAL_RATE: ['number'], 11 | NO_SQL: ['boolean'], 12 | QUERY_DEPTH_LIMIT: ['number'], 13 | QUERY_RATE_LIMIT: ['number'], 14 | SQL: ['boolean'], 15 | SQL_COLUMN_NAME: ['string'], 16 | SQL_TABLE_NAME: ['string'], 17 | SUB_FIELD: ['string'], 18 | TIME_WINDOW: ['number'], 19 | TOP_LEVEL_FIELD: ['string'], 20 | }; 21 | 22 | const keys = Object.keys(expectedTypes); 23 | for (const key of keys) { 24 | if (!expectedTypes[key].includes(typeof config[key])) { 25 | console.error( 26 | highlight( 27 | redBold( 28 | `\nERROR: INVALID TYPE FOR ${key}\nPlease adjust your qevlarConfig.json to properly account for the correct data types.\nExpected ${ 29 | expectedTypes[key] 30 | }, but got ${typeof config[key]}\n` 31 | ) 32 | ) 33 | ); 34 | return false; 35 | } 36 | } 37 | return true; 38 | } 39 | 40 | module.exports = validateConfig; 41 | -------------------------------------------------------------------------------- /src/tests/fieldDuplicationTest.js: -------------------------------------------------------------------------------- 1 | const { greenBold, redBold, yellowBold, highlight } = require('../../color'); 2 | const config = require('../../qevlarConfig.json'); 3 | const validateConfig = require('../../__tests__/validateConfig'); 4 | 5 | // Tests vulnerability to field duplication 6 | async function fieldDuplicationTest(returnToTestMenu) { 7 | const query = `{ ${config.TOP_LEVEL_FIELD}(id: ${config.ANY_TOP_LEVEL_FIELD_ID}) { ${config.SUB_FIELD} ${config.SUB_FIELD} } }`; 8 | 9 | try { 10 | const response = await fetch(config.API_URL, { 11 | method: 'POST', 12 | headers: { 'Content-Type': 'application/json' }, 13 | body: JSON.stringify({ query }), 14 | }); 15 | 16 | if (!response.ok) { 17 | throw new Error('Network response was not ok.'); 18 | } 19 | 20 | const result = await response.json(); 21 | 22 | const stringResult = JSON.stringify(result); 23 | console.log(redBold('\nTest failed:')); 24 | console.log(highlight('API accepted duplicate fields.\n')); 25 | console.log(yellowBold('API returned:'), `\n${stringResult}\n`); 26 | } catch (error) { 27 | console.log(greenBold('\nTest passed:')); 28 | console.log(highlight('API rejected duplicate fields.\n')); 29 | console.log('\nSummary of Error'); 30 | console.log('Error: ' + error.message); 31 | } 32 | 33 | if (returnToTestMenu && typeof returnToTestMenu === 'function') 34 | returnToTestMenu(); 35 | } 36 | 37 | module.exports = fieldDuplicationTest; 38 | -------------------------------------------------------------------------------- /src/tests/rateLimitTest.js: -------------------------------------------------------------------------------- 1 | const { greenBold, highlight, redBold } = require('../../color'); 2 | const config = require('../../qevlarConfig.json'); 3 | const validateConfig = require('../../__tests__/validateConfig'); 4 | 5 | // Tests rate limiting at QUERY_RATE_LIMIT 6 | async function rateLimitTest(returnToTestMenu) { 7 | validateConfig(config); 8 | const query = `{ ${config.TOP_LEVEL_FIELD} { ${config.SUB_FIELD} } }`; 9 | 10 | let reqs = 0; 11 | let lastReqTime = Date.now(); 12 | 13 | const makeRequest = async () => { 14 | reqs += 1; 15 | try { 16 | const response = await fetch(config.API_URL, { 17 | method: 'POST', 18 | headers: { 'Content-Type': 'application/json' }, 19 | body: JSON.stringify({ query }), 20 | }); 21 | 22 | if (!response.ok) { 23 | throw new Error(`HTTP error! status: ${response.status}`); 24 | } 25 | 26 | console.log( 27 | redBold('Test failed: '), 28 | `API accepted requests above rate limit (${config.QUERY_RATE_LIMIT}).` 29 | ); 30 | } catch (error) { 31 | console.log( 32 | greenBold('Test passed: '), 33 | `API did not accept requests above rate limit (${config.QUERY_RATE_LIMIT}). Error: ${error.message}` 34 | ); 35 | } 36 | }; 37 | 38 | // Request count within the time interval 39 | const now = Date.now(); 40 | 41 | if (now - lastReqTime < config.TIME_WINDOW) { 42 | console.log( 43 | greenBold('Test passed: ') + 'Requests made within time window.' 44 | ); 45 | } else { 46 | reqs = 0; 47 | lastReqTime = now; 48 | console.log( 49 | redBold('Test failed: ') + 'Time window elapsed. Resetting request count.' 50 | ); 51 | makeRequest(); 52 | } 53 | 54 | if (returnToTestMenu) returnToTestMenu(); 55 | } 56 | 57 | module.exports = rateLimitTest; 58 | -------------------------------------------------------------------------------- /src/tests/adaptiveRateLimitingTest.js: -------------------------------------------------------------------------------- 1 | const { greenBold, highlight, green } = require('../../color'); 2 | const config = require('../../qevlarConfig.json'); 3 | const validateConfig = require('../../__tests__/validateConfig'); 4 | 5 | // Tests from INITIAL_RATE up to QUERY_RATE_LIMIT at each INCREMENT 6 | async function adaptiveRateLimitingTest(returnToTestMenu) { 7 | validateConfig(config); 8 | 9 | const query = `{ ${config.TOP_LEVEL_FIELD} { ${config.SUB_FIELD} } }`; 10 | let rate = config.INITIAL_RATE; 11 | let success = true; 12 | 13 | console.log('Starting Adaptive Rate Limiting Test...'); 14 | 15 | while (success && rate < config.QUERY_RATE_LIMIT) { 16 | console.log( 17 | green('Testing at rate: ') + `${rate} requests per unit time...` 18 | ); 19 | 20 | try { 21 | for (let i = 0; i < rate; i++) { 22 | await sendGraphQLRequest(config.API_URL, query); 23 | } 24 | console.log(`Success: API accepted ${rate} requests per unit time.`); 25 | rate += config.INCREMENT; 26 | } catch (error) { 27 | success = false; 28 | console.log(greenBold('\nTest completed\n')); 29 | console.log(highlight('Summary of Test Failure:')); 30 | console.log(`- Failed at rate: ${rate} requests per unit time.`); 31 | console.log(`- Error Message: ${error.message}`); 32 | console.log( 33 | `- Possible rate limit of the API is just below ${rate} requests per unit time.\n` 34 | ); 35 | } 36 | } 37 | 38 | if (!success) { 39 | console.log( 40 | 'Consider adjusting the rate limits for better performance or resilience.' 41 | ); 42 | } else { 43 | console.log( 44 | greenBold( 45 | 'Test concluded: No rate limiting detected within the tested range.' 46 | ) 47 | ); 48 | } 49 | 50 | if (returnToTestMenu) returnToTestMenu(); 51 | } 52 | 53 | async function sendGraphQLRequest(url, query) { 54 | const response = await fetch(url, { 55 | method: 'POST', 56 | headers: { 57 | 'Content-Type': 'application/json', 58 | Accept: 'application/json', 59 | }, 60 | body: JSON.stringify({ query }), 61 | }); 62 | 63 | if (!response.ok) { 64 | throw new Error(`HTTP error! status: ${response.status}`); 65 | } 66 | 67 | return response.json(); 68 | } 69 | 70 | module.exports = adaptiveRateLimitingTest; 71 | -------------------------------------------------------------------------------- /color.js: -------------------------------------------------------------------------------- 1 | //Terminal formatting codes 2 | 3 | const color = { 4 | //GREEN 5 | green: (str) => "\u001b[" + `2;32m${str}` + "\u001b[0m", //faint green 6 | greenBold: (str) => "\u001b[" + `1;32m${str}` + "\u001b[0m", //bold green 7 | greenItalic: (str) => "\u001b[" + `3;32m${str}` + "\u001b[0m", //italic green 8 | greenHighlight: (str) => "\u001b[" + `7;32m${str}` + "\u001b[0m", //highlight green 9 | greenUnderlined: (str) => "\u001b[" + `4;32m${str}` + "\u001b[0m", //underlined green 10 | greenOut: (str) => "\u001b[" + `7;8;32m${str}` + "\u001b[0m", //green block 11 | 12 | //RED 13 | red: (str) => "\u001b[" + `2;31m${str}` + "\u001b[0m", //faint red 14 | redBold: (str) => "\u001b[" + `1;31m${str}` + "\u001b[0m", //bold red 15 | redItalic: (str) => "\u001b[" + `3;31m${str}` + "\u001b[0m", //italic red 16 | redHighlight: (str) => "\u001b[" + `7;31m${str}` + "\u001b[0m", //highlight red 17 | redUnderlined: (str) => "\u001b[" + `4;31m${str}` + "\u001b[0m", //underlined red 18 | redOut: (str) => "\u001b[" + `7;8;31m${str}` + "\u001b[0m", //red block 19 | 20 | //DARK 21 | dark: (str) => "\u001b[" + `2;30m${str}` + "\u001b[0m", //faint dark 22 | darkBold: (str) => "\u001b[" + `1;30m${str}` + "\u001b[0m", //bold dark 23 | darkItalic: (str) => "\u001b[" + `3;30m${str}` + "\u001b[0m", //italic dark 24 | darkHighlight: (str) => "\u001b[" + `7;30m${str}` + "\u001b[0m", //highlight dark 25 | darkUnderlined: (str) => "\u001b[" + `4;30m${str}` + "\u001b[0m", //underlined dark 26 | darkOut: (str) => "\u001b[" + `7;8;30m${str}` + "\u001b[0m", //dark block 27 | 28 | //YELLOW 29 | yellow: (str) => "\u001b[" + `2;33m${str}` + "\u001b[0m", //faint yellow 30 | yellowBold: (str) => "\u001b[" + `1;33m${str}` + "\u001b[0m", //bold yellow 31 | yellowItalic: (str) => "\u001b[" + `3;33m${str}` + "\u001b[0m", //italic yellow 32 | yellowHighlight: (str) => "\u001b[" + `7;33m${str}` + "\u001b[0m", //highlight yellow 33 | yellowUnderlined: (str) => "\u001b[" + `4;33m${str}` + "\u001b[0m", //underlined yellow 34 | yellowOut: (str) => "\u001b[" + `7;8;33m${str}` + "\u001b[0m", //yellow block 35 | 36 | //WHITE (normal) 37 | bold: (str) => "\u001b[" + `1m${str}` + "\u001b[0m", //bold 38 | italic: (str) => "\u001b[" + `3m${str}` + "\u001b[0m", //italic 39 | highlight: (str) => "\u001b[" + `7m${str}` + "\u001b[0m", //white highlight 40 | underlined: (str) => "\u001b[" + `4m${str}` + "\u001b[0m", //underlined 41 | whiteOut: (str) => "\u001b[" + `7;8m${str}` + "\u001b[0m", // white block 42 | }; 43 | 44 | module.exports = color; 45 | -------------------------------------------------------------------------------- /__tests__/adaptiveRateLimitingTest.js: -------------------------------------------------------------------------------- 1 | const adaptiveRateLimitingTest = require('../src/tests/adaptiveRateLimitingTest'); 2 | const fetch = require('node-fetch'); 3 | 4 | // Mocking the console.log function 5 | global.console.log = jest.fn(); 6 | 7 | // Mocking node-fetch 8 | jest.mock('node-fetch'); 9 | 10 | describe('adaptiveRateLimitingTest', () => { 11 | afterEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | it('should handle successful rate limiting test', async () => { 16 | const config = { 17 | TOP_LEVEL_FIELD: 'topField', 18 | SUB_FIELD: 'subField', 19 | API_URL: 'https://example.com/graphql', 20 | INITIAL_RATE: 10, 21 | QUERY_RATE_LIMIT: 100, 22 | INCREMENT: 10, 23 | }; 24 | 25 | const returnToTestMenu = jest.fn(); 26 | 27 | // Mocking successful fetch request 28 | fetch.mockResolvedValueOnce({ ok: true }); 29 | 30 | await adaptiveRateLimitingTest(returnToTestMenu); 31 | 32 | expect(console.log).toHaveBeenCalledWith( 33 | expect.stringContaining('Starting Adaptive Rate Limiting Test') 34 | ); 35 | expect(console.log).toHaveBeenCalledWith( 36 | expect.stringContaining('Testing at rate: ') 37 | ); 38 | expect(console.log).toHaveBeenCalledWith( 39 | expect.stringContaining( 40 | 'Success: API accepted 10 requests per unit time.' 41 | ) 42 | ); 43 | expect(console.log).toHaveBeenCalledWith( 44 | expect.stringContaining( 45 | 'Test concluded: No rate limiting detected within the tested range.' 46 | ) 47 | ); 48 | expect(returnToTestMenu).toHaveBeenCalled(); 49 | }); 50 | 51 | it('should handle rate limiting test failure', async () => { 52 | const config = { 53 | TOP_LEVEL_FIELD: 'topField', 54 | SUB_FIELD: 'subField', 55 | API_URL: 'https://example.com/graphql', 56 | INITIAL_RATE: 10, 57 | QUERY_RATE_LIMIT: 100, 58 | INCREMENT: 10, 59 | }; 60 | 61 | const returnToTestMenu = jest.fn(); 62 | 63 | // Mocking failing fetch request 64 | fetch.mockRejectedValueOnce(new Error('Rate limit exceeded')); 65 | 66 | await adaptiveRateLimitingTest(returnToTestMenu); 67 | 68 | expect(console.log).toHaveBeenCalledWith( 69 | expect.stringContaining('Starting Adaptive Rate Limiting Test') 70 | ); 71 | expect(console.log).toHaveBeenCalledWith( 72 | expect.stringContaining('Testing at rate: ') 73 | ); 74 | expect(console.log).toHaveBeenCalledWith( 75 | expect.stringContaining('Test completed') 76 | ); 77 | expect(console.log).toHaveBeenCalledWith( 78 | expect.stringContaining('Failed at rate: 20 requests per unit time.') 79 | ); 80 | expect(console.log).toHaveBeenCalledWith( 81 | expect.stringContaining('Error Message: Rate limit exceeded') 82 | ); 83 | expect(console.log).toHaveBeenCalledWith( 84 | expect.stringContaining('Possible rate limit of the API is just below') 85 | ); 86 | expect(console.log).toHaveBeenCalledWith( 87 | expect.stringContaining('Consider adjusting the rate limits') 88 | ); 89 | expect(returnToTestMenu).toHaveBeenCalled(); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/getSchema.js: -------------------------------------------------------------------------------- 1 | const config = require('../qevlarConfig.json'); 2 | const readline = require('readline'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | //Find match between types and types referenced in fields to populate CIRCULAR_REF_FIELD in qevlarConfig.json 7 | const getCircularRefField = (schema) => { 8 | const visited = new Set(); 9 | let circularRef = null; 10 | 11 | schema.types.forEach((type) => { 12 | if (type.description === 'Root Query') { 13 | type.fields.forEach((field) => { 14 | console.log('field', field); 15 | if (field.type.name) { 16 | visited.add(field.type.name); 17 | } 18 | }); 19 | } 20 | 21 | if (type.fields) { 22 | type.fields.forEach((field) => { 23 | if (visited.has(field.type.name)) { 24 | circularRef = field.type.name; 25 | } 26 | }); 27 | } 28 | }); 29 | 30 | return circularRef; 31 | }; 32 | 33 | //Extract types array to populate TOP_LEVEL_FIELD and SUB_FIELD in qevlarConfig.json 34 | const getTopAndSubField = (schema) => { 35 | const types = schema.data.__schema.types; 36 | 37 | let rootQuery; 38 | let topField; 39 | let topObjType; 40 | let subField; 41 | 42 | const circularRefField = getCircularRefField(schema.data.__schema); 43 | 44 | types.forEach((type) => { 45 | if (type.name === 'Query') { 46 | rootQuery = type; 47 | } 48 | }); 49 | 50 | topField = rootQuery.fields[0].name; 51 | topObjType = rootQuery.fields[0].type.name; 52 | 53 | const getSubField = types.find((type) => type.name === topObjType); 54 | 55 | subField = getSubField.fields[0].name; 56 | 57 | config.TOP_LEVEL_FIELD = topField; 58 | config.SUB_FIELD = subField; 59 | config.CIRCULAR_REF_FIELD = circularRefField; 60 | }; 61 | const introspectionQuery = `{ 62 | __schema { 63 | types { 64 | name 65 | description 66 | fields { 67 | name 68 | description 69 | type { 70 | name 71 | kind 72 | } 73 | } 74 | } 75 | } 76 | }`; 77 | 78 | // Dynamically generate qevlarConfig.json 79 | const modifyConig = () => { 80 | fs.writeFile( 81 | path.join(__dirname, '../qevlarConfig.json'), 82 | JSON.stringify(config, null, 2), 83 | (err) => { 84 | if (err) { 85 | console.log('error writing to qevlarConfig.json:', err); 86 | } else { 87 | console.log('qevlarConfig.json updated successfully!'); 88 | } 89 | } 90 | ); 91 | return 'success'; 92 | }; 93 | 94 | const getSchema = (url, returnToTestMenu) => { 95 | config.API_URL = url; 96 | return fetch(url, { 97 | method: 'POST', 98 | headers: { 'Content-type': 'application/json' }, 99 | body: JSON.stringify({ query: introspectionQuery }), 100 | }) 101 | .then((res) => { 102 | console.log('res status', res.status); 103 | if (res.status === 200) { 104 | return res.json(); 105 | } 106 | }) 107 | .then((data) => { 108 | getTopAndSubField(data); 109 | 110 | modifyConig(); 111 | if (returnToTestMenu) returnToTestMenu(); 112 | }) 113 | .catch((error) => { 114 | console.log('error', error); 115 | }); 116 | }; 117 | 118 | module.exports = getSchema; 119 | -------------------------------------------------------------------------------- /__tests__/maliciousInjectionTests.js: -------------------------------------------------------------------------------- 1 | const maliciousInjectionTest = require('../src/tests/maliciousInjectionTests'); 2 | const config = require('../qevlarConfig.json'); 3 | const validateConfig = require('./validateConfig'); 4 | 5 | jest.mock('../qevlarConfig.json', () => ({ 6 | API_URL: 'http://test-api.com', 7 | SQL: true, 8 | NO_SQL: true, 9 | SQL_TABLE_NAME: 'test_table', 10 | SQL_COLUMN_NAME: 'test_column', 11 | TOP_LEVEL_FIELD: 'test_field', 12 | ANY_TOP_LEVEL_FIELD_ID: '123', 13 | })); 14 | 15 | jest.mock('./validateConfig', () => jest.fn()); 16 | 17 | global.fetch = jest.fn(); 18 | 19 | describe('maliciousInjectionTest', () => { 20 | beforeEach(() => { 21 | jest.clearAllMocks(); 22 | console.log = jest.fn(); 23 | config.SQL = true; 24 | config.NO_SQL = true; 25 | }); 26 | 27 | describe('SQL', () => { 28 | it('should test SQL injection vulnerabilities', async () => { 29 | global.fetch 30 | .mockResolvedValueOnce({ ok: true }) 31 | .mockResolvedValueOnce({ ok: false }); 32 | 33 | await maliciousInjectionTest.SQL(); 34 | 35 | expect(validateConfig).toHaveBeenCalledWith(config); 36 | expect(global.fetch).toHaveBeenCalledTimes(2); 37 | expect(console.log).toHaveBeenCalledTimes(2); 38 | }); 39 | 40 | it('should not run if SQL config is false', async () => { 41 | config.SQL = false; 42 | 43 | await maliciousInjectionTest.SQL(); 44 | 45 | expect(console.log).toHaveBeenCalledWith( 46 | 'SQL config variable must be set to boolean true to execute SQL injection test.' 47 | ); 48 | expect(global.fetch).not.toHaveBeenCalled(); 49 | }); 50 | }); 51 | 52 | describe('NoSQL', () => { 53 | it('should test NoSQL injection vulnerabilities', async () => { 54 | global.fetch 55 | .mockResolvedValueOnce({ ok: true }) 56 | .mockResolvedValueOnce({ ok: false }); 57 | 58 | await maliciousInjectionTest.NoSQL(); 59 | 60 | expect(validateConfig).toHaveBeenCalledWith(config); 61 | expect(global.fetch).toHaveBeenCalledTimes(2); 62 | expect(console.log).toHaveBeenCalledTimes(2); 63 | }); 64 | 65 | it('should not run if NO_SQL config is false', async () => { 66 | config.NO_SQL = false; 67 | 68 | await maliciousInjectionTest.NoSQL(); 69 | 70 | expect(console.log).toHaveBeenCalledWith( 71 | 'NO_SQL config variable must be set to boolean true to execute NO_SQL injection test.' 72 | ); 73 | expect(global.fetch).not.toHaveBeenCalled(); 74 | }); 75 | }); 76 | 77 | describe('XSS', () => { 78 | it('should test XSS injection vulnerabilities', async () => { 79 | global.fetch 80 | .mockResolvedValueOnce({ ok: true }) 81 | .mockResolvedValueOnce({ ok: false }); 82 | 83 | await maliciousInjectionTest.XSS(); 84 | 85 | expect(validateConfig).toHaveBeenCalledWith(config); 86 | expect(global.fetch).toHaveBeenCalledTimes(2); 87 | expect(console.log).toHaveBeenCalledTimes(2); 88 | }); 89 | }); 90 | 91 | describe('OS', () => { 92 | it('should test OS command injection vulnerabilities', async () => { 93 | global.fetch 94 | .mockResolvedValueOnce({ ok: true }) 95 | .mockResolvedValueOnce({ ok: false }); 96 | 97 | await maliciousInjectionTest.OS(); 98 | 99 | expect(validateConfig).toHaveBeenCalledWith(config); 100 | expect(global.fetch).toHaveBeenCalledTimes(2); 101 | expect(console.log).toHaveBeenCalledTimes(2); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /__tests__/fieldDuplicationTest.js: -------------------------------------------------------------------------------- 1 | const fieldDuplicationTest = require('../src/tests/fieldDuplicationTest'); 2 | const config = require('../qevlarConfig.json'); 3 | const validateConfig = require('./validateConfig'); 4 | const { greenBold, redBold, yellowBold, highlight } = require('../color'); 5 | 6 | jest.mock('../color', () => ({ 7 | greenBold: jest.fn((text) => text), 8 | redBold: jest.fn((text) => text), 9 | highlight: jest.fn((text) => text), 10 | })); 11 | 12 | jest.mock('../qevlarConfig.json', () => ({ 13 | API_URL: 'http://test-api.com', 14 | TOP_LEVEL_FIELD: 'test_field', 15 | ANY_TOP_LEVEL_FIELD_ID: '123', 16 | SUB_FIELD: 'sub_field', 17 | })); 18 | 19 | jest.mock('./validateConfig', () => jest.fn()); 20 | 21 | // Mock fetch function 22 | global.fetch = jest.fn(); 23 | 24 | describe('fieldDuplicationTest', () => { 25 | beforeEach(() => { 26 | jest.clearAllMocks(); 27 | console.log = jest.fn(); 28 | }); 29 | 30 | it('should log failure if API accepts duplicate fields', async () => { 31 | const mockResponse = { 32 | ok: true, 33 | json: jest.fn().mockResolvedValue({ data: 'mockData' }), 34 | }; 35 | global.fetch.mockResolvedValueOnce(mockResponse); 36 | 37 | const returnToTestMenu = jest.fn(); 38 | 39 | await fieldDuplicationTest(returnToTestMenu); 40 | 41 | const expectedQuery = `{ test_field(id: 123) { sub_field sub_field } }`; 42 | 43 | expect(global.fetch).toHaveBeenCalledWith(config.API_URL, { 44 | method: 'POST', 45 | headers: { 'Content-Type': 'application/json' }, 46 | body: JSON.stringify({ query: expectedQuery }), 47 | }); 48 | 49 | expect(console.log).toHaveBeenCalledWith(redBold('\nTest failed:')); 50 | expect(console.log).toHaveBeenCalledWith( 51 | highlight('API accepted duplicate fields.\n') 52 | ); 53 | expect(console.log).toHaveBeenCalledWith( 54 | yellowBold('API returned:'), 55 | `\n{"data":"mockData"}\n` 56 | ); 57 | 58 | expect(returnToTestMenu).toHaveBeenCalled(); 59 | }); 60 | 61 | it('should log success if API rejects duplicate fields', async () => { 62 | global.fetch.mockRejectedValueOnce( 63 | new Error('Network response was not ok.') 64 | ); 65 | 66 | const returnToTestMenu = jest.fn(); 67 | 68 | await fieldDuplicationTest(returnToTestMenu); 69 | 70 | expect(global.fetch).toHaveBeenCalled(); 71 | 72 | expect(console.log).toHaveBeenCalledWith(greenBold('\nTest passed:')); 73 | expect(console.log).toHaveBeenCalledWith( 74 | highlight('API rejected duplicate fields.\n') 75 | ); 76 | expect(console.log).toHaveBeenCalledWith('\nSummary of Error'); 77 | expect(console.log).toHaveBeenCalledWith( 78 | 'Error: Network response was not ok.' 79 | ); 80 | 81 | expect(returnToTestMenu).toHaveBeenCalled(); 82 | }); 83 | 84 | it('should call returnToTestMenu if it is a function', async () => { 85 | global.fetch.mockResolvedValueOnce({ 86 | ok: true, 87 | json: jest.fn().mockResolvedValue({ data: 'mockData' }), 88 | }); 89 | 90 | const returnToTestMenu = jest.fn(); 91 | 92 | await fieldDuplicationTest(returnToTestMenu); 93 | 94 | expect(returnToTestMenu).toHaveBeenCalled(); 95 | }); 96 | 97 | it('should not call returnToTestMenu if it is not a function', async () => { 98 | global.fetch.mockResolvedValueOnce({ 99 | ok: true, 100 | json: jest.fn().mockResolvedValue({ data: 'mockData' }), 101 | }); 102 | 103 | const returnToTestMenu = 'not a function'; 104 | 105 | await fieldDuplicationTest(returnToTestMenu); 106 | 107 | expect(returnToTestMenu).not.toBeCalled(); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /__tests__/depthLimitTests.js: -------------------------------------------------------------------------------- 1 | const { greenBold, redBold, highlight } = require('../color'); 2 | 3 | const depthLimitTest = require('../src/tests/depthLimitTests'); 4 | const config = require('../qevlarConfig.json'); 5 | const validateConfig = require('./validateConfig'); 6 | 7 | jest.mock('../color', () => ({ 8 | greenBold: jest.fn((text) => text), 9 | redBold: jest.fn((text) => text), 10 | highlight: jest.fn((text) => text), 11 | })); 12 | 13 | jest.mock('../qevlarConfig.json', () => ({ 14 | API_URL: 'http://test-api.com', 15 | TOP_LEVEL_FIELD: 'test_field', 16 | ANY_TOP_LEVEL_FIELD_ID: '123', 17 | CIRCULAR_REF_FIELD: 'circular_field', 18 | QUERY_DEPTH_LIMIT: 3, 19 | })); 20 | 21 | jest.mock('./validateConfig', () => jest.fn()); 22 | 23 | // Mock fetch function 24 | global.fetch = jest.fn(); 25 | 26 | describe('depthLimitTest', () => { 27 | beforeEach(() => { 28 | jest.clearAllMocks(); 29 | console.log = jest.fn(); 30 | }); 31 | 32 | describe('max', () => { 33 | it('should validate config and test one level deeper than QUERY_DEPTH_LIMIT', async () => { 34 | global.fetch.mockResolvedValueOnce({ status: 400 }); 35 | 36 | await depthLimitTest.max(); 37 | 38 | expect(validateConfig).toHaveBeenCalledWith(config); 39 | expect(global.fetch).toHaveBeenCalledTimes(1); 40 | expect(console.log).toHaveBeenCalledWith( 41 | greenBold('Test passed: ') + 42 | highlight( 43 | `Query blocked. Query depth exceeded depth limit of ${config.QUERY_DEPTH_LIMIT}.` 44 | ) 45 | ); 46 | }); 47 | 48 | it('should log failure if query is not blocked', async () => { 49 | global.fetch.mockResolvedValueOnce({ status: 200 }); 50 | 51 | await depthLimitTest.max(); 52 | 53 | expect(validateConfig).toHaveBeenCalledWith(config); 54 | expect(global.fetch).toHaveBeenCalledTimes(1); 55 | expect(console.log).toHaveBeenCalledWith( 56 | redBold('Test failed: ') + 57 | highlight( 58 | `Query depth was over limit of ${config.QUERY_DEPTH_LIMIT}, yet was not blocked.` 59 | ) 60 | ); 61 | }); 62 | }); 63 | 64 | describe('incremental', () => { 65 | it('should validate config and test each depth level up to QUERY_DEPTH_LIMIT', async () => { 66 | global.fetch.mockResolvedValue({ status: 200 }); 67 | 68 | await depthLimitTest.incremental(); 69 | 70 | expect(validateConfig).toHaveBeenCalledWith(config); 71 | expect(global.fetch).toHaveBeenCalledTimes(config.QUERY_DEPTH_LIMIT); 72 | for (let i = 1; i <= config.QUERY_DEPTH_LIMIT; i++) { 73 | expect(console.log).toHaveBeenCalledWith( 74 | greenBold(`------> Query at depth ${i} complete.<-------`) 75 | ); 76 | } 77 | }); 78 | 79 | it('should log success if query is blocked at depth above QUERY_DEPTH_LIMIT', async () => { 80 | global.fetch 81 | .mockResolvedValueOnce({ status: 200 }) 82 | .mockResolvedValueOnce({ status: 200 }) 83 | .mockResolvedValueOnce({ status: 400 }); 84 | 85 | await depthLimitTest.incremental(); 86 | 87 | expect(validateConfig).toHaveBeenCalledWith(config); 88 | expect(global.fetch).toHaveBeenCalledTimes(3); 89 | expect(console.log).toHaveBeenCalledWith( 90 | redBold(`------> Query at depth 4 incomplete.<-------`) 91 | ); 92 | expect(console.log).toHaveBeenCalledWith( 93 | greenBold('Test passed: ') + 94 | highlight( 95 | `Query blocked. Depth limited above ${config.QUERY_DEPTH_LIMIT} queries.\n` 96 | ) 97 | ); 98 | }); 99 | 100 | it('should log failure if query is not limited to QUERY_DEPTH_LIMIT', async () => { 101 | global.fetch.mockResolvedValue({ status: 200 }); 102 | 103 | await depthLimitTest.incremental(); 104 | 105 | expect(validateConfig).toHaveBeenCalledWith(config); 106 | expect(global.fetch).toHaveBeenCalledTimes(config.QUERY_DEPTH_LIMIT); 107 | expect(console.log).toHaveBeenCalledWith( 108 | redBold('Test failed: ') + 109 | highlight(`Query depth not limited to ${config.QUERY_DEPTH_LIMIT}.\n`) 110 | ); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /qevlar: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { redBold, bold, darkBold, yellowBold } = require('./color.js'); 4 | const readline = require('readline'); 5 | const qevlarLogo = ` 6 | 7 | ██████ ███████ ██ ██ ██ █████ ██████ 8 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ 9 | ██ ██ █████ ██ ██ ██ ███████ ██████ 10 | ██ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ 11 | ██████ ███████ ████ ███████ ██ ██ ██ ██ 12 | ▀▀ 13 | `; 14 | 15 | const maliciousInjectionTest = require('./src/tests/maliciousInjectionTests.js'); 16 | const fieldDuplicationTest = require('./src/tests/fieldDuplicationTest.js'); 17 | const depthLimitTest = require('./src/tests/depthLimitTests.js'); 18 | const adaptiveRateLimitingTest = require('./src/tests/adaptiveRateLimitingTest.js'); 19 | const rateLimitTest = require('./src/tests/rateLimitTest.js'); 20 | const { batchTest } = require('./src/tests/queryBatchTest.js'); 21 | const getSchema = require('./src/getSchema.js'); 22 | 23 | const tests = { 24 | 0: { 25 | name: 'Generate Config', 26 | function: generateConfig, 27 | }, 28 | 1: { 29 | name: 'Rate Limit Test', 30 | function: rateLimitTest, 31 | }, 32 | 2: { 33 | name: 'Adaptive Rate Limiting Test', 34 | function: adaptiveRateLimitingTest, 35 | }, 36 | 3: { 37 | name: 'Fixed Depth Test', 38 | function: depthLimitTest.max, 39 | }, 40 | 4: { 41 | name: 'Incremental Depth Test', 42 | function: depthLimitTest.incremental, 43 | }, 44 | 5: { 45 | name: 'Field Duplication Test', 46 | function: fieldDuplicationTest, 47 | }, 48 | 6: { 49 | name: 'Query Batch Test', 50 | function: batchTest, 51 | }, 52 | 7: { 53 | name: 'SQL Malicious Injection Test', 54 | function: maliciousInjectionTest.SQL, 55 | }, 56 | 8: { 57 | name: 'NoSQL Malicious Injection Test', 58 | function: maliciousInjectionTest.NoSQL, 59 | }, 60 | 9: { 61 | name: 'Cross-Site Scripting Injection Test', 62 | function: maliciousInjectionTest.XSS, 63 | }, 64 | 10: { 65 | name: 'OS Command Injection Test', 66 | function: maliciousInjectionTest.OS, 67 | }, 68 | }; 69 | 70 | // To return to test menu after test completion 71 | function runTest(testKey, rl) { 72 | console.log(`\nRunning ${tests[testKey].name}...\n`); 73 | tests[testKey].function(() => { 74 | console.log('Choose another test or press Q to exit.'); 75 | rl.close(); 76 | listTestsAndRunSelection(); 77 | }); 78 | } 79 | 80 | function listTestsAndRunSelection() { 81 | console.log( 82 | bold( 83 | '\n\n><><><><><>< ' + 84 | bold('W E L C O M E T O Q E V L A R') + 85 | ' ><><><><><><\n' 86 | ) 87 | ); 88 | console.log(yellowBold('\nAvailable Tests:')); 89 | console.log(darkBold('═══════════════════════════════')); 90 | for (const test in tests) { 91 | console.log(`${test} : ${tests[test].name}`); 92 | } 93 | console.log('Q: Exit qevlar testing library'); 94 | console.log(darkBold('═══════════════════════════════')); 95 | 96 | const rl = readline.createInterface({ 97 | input: process.stdin, 98 | output: process.stdout, 99 | terminal: false, 100 | }); 101 | 102 | rl.question( 103 | '\nEnter the number of the test to run or Q to quit: \n', 104 | (input) => { 105 | const testKey = input.trim().toUpperCase(); 106 | if (testKey === 'Q') { 107 | console.log(redBold('\nThank you for using')); 108 | console.log(`${qevlarLogo}`); 109 | console.log(redBold('\nExiting...')); 110 | rl.close(); 111 | } else if (tests[testKey]) { 112 | runTest(testKey, rl); 113 | } else { 114 | console.log('Invalid selection.'); 115 | rl.close(); 116 | listTestsAndRunSelection(); 117 | } 118 | } 119 | ); 120 | } 121 | 122 | // To auto-generate qevlarConfig.json 123 | async function generateConfig() { 124 | return new Promise((resolve, reject) => { 125 | const rl = readline.createInterface({ 126 | input: process.stdin, 127 | output: process.stdout, 128 | terminal: false, 129 | }); 130 | rl.question('\nPlease submit the url to your API:\n', async (apiUrl) => { 131 | try { 132 | const result = await getSchema(apiUrl, listTestsAndRunSelection); 133 | if (result === 'success') { 134 | resolve(); 135 | rl.close(); 136 | } 137 | } catch (error) { 138 | console.log('error in getSchema', error); 139 | rl.close(); 140 | reject(error); 141 | } 142 | }); 143 | }); 144 | } 145 | 146 | listTestsAndRunSelection(); 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 |