diff --git a/.prettierrc b/.prettierrc index e98ecea..d55c191 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,5 +7,13 @@ "singleQuote": true, "tabWidth": 2, "trailingComma": "none", - "useTabs": false + "useTabs": false, + "overrides": [ + { + "files": "*.hbs", + "options": { + "singleQuote": false + } + } + ] } diff --git a/config/config.js b/config/config.js index 7eae52c..286fb7f 100644 --- a/config/config.js +++ b/config/config.js @@ -24,7 +24,7 @@ module.exports = { * @optional */ description: 'IP Lookup Integration for Shodan', - entityTypes: ['IPv4', 'IPv6'], + entityTypes: ['IPv4', 'IPv6', 'IPv4CIDR'], /** * An array of style files (css or less) that will be included for your integration. Any styles specified in * the below files can be used in your custom template. @@ -64,7 +64,7 @@ module.exports = { ca: '', // An HTTP proxy to be used. Supports proxy Auth with Basic Auth, identical to support for // the url parameter (by embedding the auth info in the uri) - proxy: "" + proxy: '' }, logging: { level: 'info' //trace, debug, info, warn, error, fatal diff --git a/integration.js b/integration.js index b48a49d..206f37f 100644 --- a/integration.js +++ b/integration.js @@ -3,7 +3,21 @@ const request = require('postman-request'); const fs = require('fs'); const Bottleneck = require('bottleneck'); -const _ = require('lodash'); +const { + flow, + get, + partition, + isArray, + isEmpty, + join, + map, + size, + identity, + sortBy, + toArray, + values, + take +} = require('lodash/fp'); const cache = require('memory-cache'); const config = require('./config/config'); @@ -14,6 +28,7 @@ let Logger; let requestWithDefaults; const IGNORED_IPS = new Set(['127.0.0.1', '255.255.255.255', '0.0.0.0']); +const MAX_FACET_RESULTS = 1000; function doLookup(entities, options, cb) { let limiter = bottlneckApiKeyCache.get(options.apiKey); @@ -36,19 +51,43 @@ function doLookup(entities, options, cb) { (entity) => !entity.isPrivateIP && !IGNORED_IPS.has(entity.value) ); + let requestOptions; validEntities.forEach((entity) => { - let requestOptions = { - uri: 'https://api.shodan.io/shodan/host/' + entity.value + '?key=' + options.apiKey, - method: 'GET', - json: true, - maxResponseSize: 2000000 // 2MB in bytes - }; + if (entity.type === 'IPv4CIDR') { + requestOptions = { + uri: 'https://api.shodan.io/shodan/host/search', + qs: { + key: options.apiKey, + query: `net:${entity.value}`, + facets: `vuln:${MAX_FACET_RESULTS},port:${MAX_FACET_RESULTS},ip:${MAX_FACET_RESULTS},org:${MAX_FACET_RESULTS},product:${MAX_FACET_RESULTS}` + }, + method: 'GET', + json: true, + maxResponseSize: 10000000 // 10MB in bytes + }; + } else { + requestOptions = { + uri: `https://api.shodan.io/shodan/host/${entity.value}`, + qs: { + key: options.apiKey + }, + method: 'GET', + json: true, + maxResponseSize: 2000000 // 2MB in bytes + }; + } + + Logger.trace({ requestOptions }, 'Request Options'); limiter.submit(requestEntity, entity, requestOptions, (err, result) => { const maxRequestQueueLimitHit = - (_.isEmpty(err) && _.isEmpty(result)) || + (isEmpty(err) && isEmpty(result)) || (err && err.message === 'This job has been dropped by Bottleneck'); + if (entity.type === 'IPv4CIDR' && result && result.body) { + result = assembleCIDRResults(result); + } + requestResults.push([ err, maxRequestQueueLimitHit ? { ...result, entity, limitReached: true } : result @@ -56,7 +95,7 @@ function doLookup(entities, options, cb) { if (requestResults.length === validEntities.length) { const [errs, results] = transpose2DArray(requestResults); - const errors = errs.filter((err) => !_.isEmpty(err)); + const errors = errs.filter((err) => !isEmpty(err)); if (errors.length) { Logger.trace({ errors }, 'Something went wrong'); @@ -66,8 +105,7 @@ function doLookup(entities, options, cb) { }); } - // filter out empty results - const filteredResults = results.filter((result) => !_.isEmpty(result)); + const filteredResults = results.filter((result) => !isEmpty(result)); const lookupResults = filteredResults.map((result) => { if (result.limitReached) { @@ -122,19 +160,16 @@ const requestEntity = (entity, requestOptions, callback) => Logger.trace({ body }, 'Result of Lookup'); if (res.statusCode === 200) { - // we got data! return callback(null, { entity, body }); } else if (res.statusCode === 404) { - // no result found return callback(null, { entity, body: null }); } else if (res.statusCode === 401) { - // no result found return callback({ detail: 'Unauthorized: The provided API key is invalid.' }); @@ -227,16 +262,31 @@ function validateOptions(userOptions, cb) { cb(null, errors); } +const assembleCIDRResults = (apiResponse) => { + if (apiResponse.body.total < 1) { + return { + entity: apiResponse.entity, + data: { + summary: ['No Results Found'], + details: { tags: ['No Results Found'] } + } + }; + } + + let resultsFacets = { + ...apiResponse.body.facets + }; + + return { entity: apiResponse.entity, body: resultsFacets, limitReached: false }; +}; + /** * Creates the Summary Tags (currently just tags for ports) * @param apiResponse * @returns {string[]} */ const createSummary = (apiResponse) => { - Logger.trace({ apiResponse }, 'Creating Summary Tags'); - const tags = createPortTags(apiResponse); - Logger.trace({ tags }, 'Summary Tags Created'); if (Array.isArray(apiResponse.body.tags)) { const apiTags = apiResponse.body.tags; @@ -250,6 +300,8 @@ const createSummary = (apiResponse) => { } } + if (apiResponse.body.totalVuln) tags.push(`Vulnerabilities: ${apiResponse.body.totalVuln}`); + Logger.trace({ tags }, 'final tags'); return tags; }; @@ -279,56 +331,52 @@ const createSummary = (apiResponse) => { * @param apiResponse * @returns {[string]} */ + const createPortTags = (apiResponse) => { - Logger.trace({ apiResponse }, 'Creating Port Tags'); - const portTags = []; - const ports = Array.from(apiResponse.body.ports); + let getPorts; - // sort the ports from smallest to largest - ports.sort((a, b) => { - return a - b; - }); + if (Array.isArray(get('body.ports', apiResponse))) { + getPorts = get('body.ports'); + } else { + getPorts = flow(get('body.port'), map('value')); + } + + const ports = flow( + getPorts, + (data) => (isArray(data) ? data : values(data)), + toArray, + sortBy(identity) + )(apiResponse); - if (ports.length === 0) { + if (isEmpty(ports)) { return [`No Open Ports`]; - } else if (ports.length <= 10) { - return [`Ports: ${ports.join(', ')}`]; - } else { - let splitIndex = ports.length; - for (let i = 0; i < ports.length; i++) { - if (ports[i] > 1024) { - splitIndex = i; - break; - } - } + } - // ports array is for reserved ports - // ephemeralPorts is for ephemeral ports ( ports > 1024) - const ephemeralPorts = ports.splice(splitIndex); - const numEphemeralPorts = ephemeralPorts.length; - const firstTenReservedPorts = ports.slice(0, 10); - const extraReservedCount = ports.length > 10 ? ports.length - 10 : 0; - - if (firstTenReservedPorts.length > 0) { - portTags.push( - `Reserved Ports: ${firstTenReservedPorts.join(', ')}${ - extraReservedCount > 0 ? ', +' + extraReservedCount + ' more' : '' - }` - ); - } + const [reservedPorts, ephemeralPorts] = partition((port) => port <= 1024)(ports); - if (numEphemeralPorts > 0) { - portTags.push(`${numEphemeralPorts} ephemeral ports`); - } + var portTags = []; + + if (!isEmpty(reservedPorts)) { + const visibleReservedPorts = take(10)(reservedPorts); + const hiddenCount = Math.max(size(reservedPorts) - 10, 0); + const visibleText = join(', ')(visibleReservedPorts); + + portTags.push( + `Reserved Ports: ${visibleText}${hiddenCount > 0 ? `, +${hiddenCount} more` : ''}` + ); + } - Logger.trace({ portTags }, 'Port Tags Created'); - return portTags; + if (!isEmpty(ephemeralPorts)) { + portTags.push(`${size(ephemeralPorts)} ephemeral ports`); } + + Logger.trace({ portTags }, 'Port Tags Created'); + return portTags; }; module.exports = { - doLookup, startup, + doLookup, validateOptions, onMessage: retryEntity }; diff --git a/logging.js b/logging.js new file mode 100644 index 0000000..e323f1f --- /dev/null +++ b/logging.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const { flow, reduce } = require('lodash/fp'); + +const writeToDevRunnerResults = (loggingLevel) => (...content) => + fs.appendFileSync( + 'devRunnerResults.json', + '\n' + JSON.stringify({ SOURCE: `Logger.${loggingLevel}`, content }, null, 2) + ); + +let logger = flow( + reduce((agg, level) => ({ ...agg, [level]: writeToDevRunnerResults(level) }), {}) +)(['trace', 'debug', 'info', 'warn', 'error', 'fatal']); + +const setLogger = (_logger) => { + logger = _logger; +}; + +const getLogger = () => logger; + +module.exports = { setLogger, getLogger }; diff --git a/package-lock.json b/package-lock.json index a83aeb8..c62eaca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "shodan", - "version": "3.4.2", + "version": "3.4.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -15,9 +15,9 @@ } }, "@postman/tough-cookie": { - "version": "4.1.2-postman.1", - "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.2-postman.1.tgz", - "integrity": "sha512-keOKL3RQohnH5K4GNGSV7JE8+SU/ktWJ3h9ulqttOGUWCZWcbUOtMNXkC3PQ/R1hIa+2qEfh0/5NCcAhWqeMTQ==", + "version": "4.1.3-postman.1", + "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz", + "integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==", "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -275,12 +275,12 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "postman-request": { - "version": "2.88.1-postman.32", - "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.32.tgz", - "integrity": "sha512-Zf5D0b2G/UmnmjRwQKhYy4TBkuahwD0AMNyWwFK3atxU1u5GS38gdd7aw3vyR6E7Ii+gD//hREpflj2dmpbE7w==", + "version": "2.88.1-postman.33", + "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.33.tgz", + "integrity": "sha512-uL9sCML4gPH6Z4hreDWbeinKU0p0Ke261nU7OvII95NU22HN6Dk7T/SaVPaj6T4TsQqGKIFw6/woLZnH7ugFNA==", "requires": { "@postman/form-data": "~3.1.1", - "@postman/tough-cookie": "~4.1.2-postman.1", + "@postman/tough-cookie": "~4.1.3-postman.1", "@postman/tunnel-agent": "^0.6.3", "aws-sign2": "~0.7.0", "aws4": "^1.12.0", @@ -405,4 +405,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index f35a7bf..ffafb19 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "shodan", - "version": "3.4.2", + "version": "3.4.3", "main": "./integration.js", "private": true, "dependencies": { "bottleneck": "^2.19.5", "lodash": "^4.17.21", "memory-cache": "^0.2.0", - "postman-request": "^2.88.1-postman.32" + "postman-request": "^2.88.1-postman.33" } -} \ No newline at end of file +} diff --git a/styles/shodan.less b/styles/shodan.less index 6a25106..63d4e96 100644 --- a/styles/shodan.less +++ b/styles/shodan.less @@ -5,6 +5,21 @@ padding: 3px 3px 2px 3px; display: inline-block; } + +.scrollable-block { + position: relative; + overflow-y: auto; + min-height: 10px; + max-height: 150px; + margin-bottom: 5px; + width: 100%; + padding: 3px 0px 3px 0px; + background-color: #f9f9f9; + border-radius: 3px; + scrollbar-width: thin; + scrollbar-color: rgba(155, 155, 155, 0.7) transparent; +} + .summary-try-again-button { padding: 0px 3px; background-color: rgba(0, 0, 0, 0.3); @@ -24,11 +39,24 @@ margin: 0px 0px 1px 3px; } +.p-block { + word-break: break-word; +} + .data-block { border-left: 1px solid #eee; padding-left: 5px; margin-left: 0px; } + +ul { + list-style-image: none; + list-style-type: none; + list-style-position: outside; + padding: 5px 10px; + margin: 0px; +} + div.service-pill { display: inline-block; line-height: 2em; diff --git a/templates/shodan-block.hbs b/templates/shodan-block.hbs index 762a929..1521693 100644 --- a/templates/shodan-block.hbs +++ b/templates/shodan-block.hbs @@ -1,7 +1,9 @@ {{#if details.limitReached}} -
- This entity could not be searched as you've temporarily reached your Shodan Search Limit. You + This entity could not be searched as you"ve temporarily reached your Shodan Search Limit. You can retry your search by pressing the "Try Again" button.
- This Entity does not exist in Shodan. -
- {{else}} -