From 0579d3fa93e5566cb68efb23b17d601271c1022f Mon Sep 17 00:00:00 2001 From: Jon Penwood Date: Mon, 17 Feb 2020 14:47:51 -0600 Subject: [PATCH 01/10] Bumped package version, and replaced async with bottleneck. --- integration.js | 176 +++++++++++++++++++++++++------------------------ package.json | 6 +- 2 files changed, 94 insertions(+), 88 deletions(-) diff --git a/integration.js b/integration.js index 16bd198..6d4165e 100644 --- a/integration.js +++ b/integration.js @@ -1,16 +1,20 @@ -'use strict'; +"use strict"; -const request = require('request'); -const config = require('./config/config'); -const async = require('async'); -const fs = require('fs'); +const request = require("request"); +const config = require("./config/config"); +const fs = require("fs"); +const Bottleneck = require("bottleneck"); + +const limiter = new Bottleneck({ + maxConcurrent: 1, + minTime: 1050 +}); let Logger; let requestWithDefaults; -const MAX_PARALLEL_LOOKUPS = 10; -const IGNORED_IPS = new Set(['127.0.0.1', '255.255.255.255', '0.0.0.0']); +const IGNORED_IPS = new Set(["127.0.0.1", "255.255.255.255", "0.0.0.0"]); /** * @@ -19,110 +23,111 @@ const IGNORED_IPS = new Set(['127.0.0.1', '255.255.255.255', '0.0.0.0']); * @param cb */ function doLookup(entities, options, cb) { - let lookupResults = []; - let tasks = []; + let requestResults = []; - Logger.trace({ entities: entities }, 'doLookup'); + Logger.trace({ entities }, "doLookup"); entities.forEach((entity) => { if (!entity.isPrivateIP && !IGNORED_IPS.has(entity.value)) { - //do the lookup let requestOptions = { - uri: 'https://api.shodan.io/shodan/host/' + entity.value + '?key=' + options.apiKey, - method: 'GET', + uri: + "https://api.shodan.io/shodan/host/" + entity.value + "?key=" + options.apiKey, + method: "GET", json: true }; - tasks.push(function(done) { - requestWithDefaults(requestOptions, function(error, res, body) { - if (error || typeof res === 'undefined') { - Logger.error({ err: error }, 'HTTP Request Failed'); - done({ - detail: 'HTTP Request Failed', - err: error - }); - return; - } - - Logger.trace({ body: body }, 'Result of Lookup'); - - if (res.statusCode === 200) { - // we got data! - return done(null, { - entity: entity, - body: body - }); - } else if (res.statusCode === 404) { - // no result found - return done(null, { - entity: entity, - body: null - }); - } else if (res.statusCode === 503) { - // reached request limit - return done({ - detail: 'Request Limit Reached' - }); - } else { - return done({ - detail: 'Unexpected HTTP Status Received', - httpStatus: res.statusCode, - body: body - }); - } - }); + limiter.submit(requestEntity, requestOptions, (err, result) => { + requestResults.push([err, result]); + if (requestResults.length === entities.length) { + const [errs, results] = rotateResults(results); + if (errs.length) return cb(errs[0]); + + const lookupResults = results.map(({ entity, body }) => ({ + entity, + data: body && { + summary: [], + details: body + } + })); + + cb(null, lookupResults); + } }); } }); +} - async.parallelLimit(tasks, MAX_PARALLEL_LOOKUPS, (err, results) => { - if (err) { - cb(err); - return; +const requestEntity = (requestOptions, callback) => + requestWithDefaults(requestOptions, (err, res, body) => { + if (err || typeof res === "undefined") { + Logger.error({ err }, "HTTP Request Failed"); + return callback({ + detail: "HTTP Request Failed", + err + }); } - results.forEach((result) => { - if (result.body === null) { - lookupResults.push({ - entity: result.entity, - data: null - }); - } else { - lookupResults.push({ - entity: result.entity, - data: { - summary: [], - details: result.body - } - }); - } - }); + Logger.trace({ body }, "Result of Lookup"); - cb(null, lookupResults); + 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 === 503) { + // reached request limit + return callback({ + detail: "Request Limit Reached" + }); + } else { + return callback({ + detail: "Unexpected HTTP Status Received", + httpStatus: res.statusCode, + body + }); + } }); -} + +const rotateResults = (results) => + results.reduce( + (agg, [err, result]) => [ + [...agg[0], err], + [...agg[1], result] + ], + [[], []] + ); function startup(logger) { Logger = logger; let defaults = {}; - if (typeof config.request.cert === 'string' && config.request.cert.length > 0) { + if (typeof config.request.cert === "string" && config.request.cert.length > 0) { defaults.cert = fs.readFileSync(config.request.cert); } - if (typeof config.request.key === 'string' && config.request.key.length > 0) { + if (typeof config.request.key === "string" && config.request.key.length > 0) { defaults.key = fs.readFileSync(config.request.key); } - if (typeof config.request.passphrase === 'string' && config.request.passphrase.length > 0) { + if ( + typeof config.request.passphrase === "string" && + config.request.passphrase.length > 0 + ) { defaults.passphrase = config.request.passphrase; } - if (typeof config.request.ca === 'string' && config.request.ca.length > 0) { + if (typeof config.request.ca === "string" && config.request.ca.length > 0) { defaults.ca = fs.readFileSync(config.request.ca); } - if (typeof config.request.proxy === 'string' && config.request.proxy.length > 0) { + if (typeof config.request.proxy === "string" && config.request.proxy.length > 0) { defaults.proxy = config.request.proxy; } @@ -132,12 +137,13 @@ function startup(logger) { function validateOptions(userOptions, cb) { let errors = []; if ( - typeof userOptions.apiKey.value !== 'string' || - (typeof userOptions.apiKey.value === 'string' && userOptions.apiKey.value.length === 0) + typeof userOptions.apiKey.value !== "string" || + (typeof userOptions.apiKey.value === "string" && + userOptions.apiKey.value.length === 0) ) { errors.push({ - key: 'apiKey', - message: 'You must provide a Shodan API key' + key: "apiKey", + message: "You must provide a Shodan API key" }); } @@ -145,7 +151,7 @@ function validateOptions(userOptions, cb) { } module.exports = { - doLookup: doLookup, - startup: startup, - validateOptions: validateOptions + doLookup, + startup, + validateOptions }; diff --git a/package.json b/package.json index b50e791..55eafa6 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "main": "./integration.js", "name": "shodan", - "version": "3.0.1", + "version": "3.0.2-beta", "private": true, "dependencies": { - "request": "^2.88", - "async": "^2.6" + "bottleneck": "^2.19.5", + "request": "^2.88" } } From 6ae92ec4e8360aac1bf0713573c6ff27d8e17090 Mon Sep 17 00:00:00 2001 From: Jon Penwood Date: Mon, 24 Feb 2020 15:47:20 -0600 Subject: [PATCH 02/10] Updated config for the API key option to be a password type and added onDemandOnly, as well as improved error handling. --- config/config.js | 190 +++++++++++++++++++++++------------------------ integration.js | 15 ++-- 2 files changed, 105 insertions(+), 100 deletions(-) diff --git a/config/config.js b/config/config.js index aad4d6b..8698010 100644 --- a/config/config.js +++ b/config/config.js @@ -1,99 +1,99 @@ module.exports = { - /** - * Name of the integration which is displayed in the Polarity integrations user interface - * - * @type String - * @required - */ - name: "Shodan", - /** - * The acronym that appears in the notification window when information from this integration - * is displayed. Note that the acronym is included as part of each "tag" in the summary information - * for the integration. As a result, it is best to keep it to 4 or less characters. The casing used - * here will be carried forward into the notification window. - * - * @type String - * @required - */ - acronym: "SHO", - /** - * Description for this integration which is displayed in the Polarity integrations user interface - * - * @type String - * @optional - */ - description: "IP Lookup Integration for Shodan", - entityTypes: ['IPv4', 'IPv6'], - /** - * 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. - * - * @type Array - * @optional - */ - "styles": [ - "./styles/shodan.less" - ], - /** - * Provide custom component logic and template for rendering the integration details block. If you do not - * provide a custom template and/or component then the integration will display data as a table of key value - * pairs. - * - * @type Object - * @optional - */ - block: { - component: { - file: "./components/shodan-block.js" - }, - template: { - file: "./templates/shodan-block.hbs" - } + /** + * Name of the integration which is displayed in the Polarity integrations user interface + * + * @type String + * @required + */ + name: "Shodan", + /** + * The acronym that appears in the notification window when information from this integration + * is displayed. Note that the acronym is included as part of each "tag" in the summary information + * for the integration. As a result, it is best to keep it to 4 or less characters. The casing used + * here will be carried forward into the notification window. + * + * @type String + * @required + */ + acronym: "SHO", + /** + * Description for this integration which is displayed in the Polarity integrations user interface + * + * @type String + * @optional + */ + description: "IP Lookup Integration for Shodan", + entityTypes: ["IPv4", "IPv6"], + /** + * 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. + * + * @type Array + * @optional + */ + styles: ["./styles/shodan.less"], + /** + * Provide custom component logic and template for rendering the integration details block. If you do not + * provide a custom template and/or component then the integration will display data as a table of key value + * pairs. + * + * @type Object + * @optional + */ + block: { + component: { + file: "./components/shodan-block.js" }, - summary: { - component: { - file: './components/shodan-summary.js' - }, - template: { - file: './templates/shodan-summary.hbs' - } + template: { + file: "./templates/shodan-block.hbs" + } + }, + summary: { + component: { + file: "./components/shodan-summary.js" }, - request: { - // Provide the path to your certFile. Leave an empty string to ignore this option. - // Relative paths are relative to the VT integration's root directory - cert: '', - // Provide the path to your private key. Leave an empty string to ignore this option. - // Relative paths are relative to the VT integration's root directory - key: '', - // Provide the key passphrase if required. Leave an empty string to ignore this option. - // Relative paths are relative to the VT integration's root directory - passphrase: '', - // Provide the Certificate Authority. Leave an empty string to ignore this option. - // Relative paths are relative to the VT integration's root directory - 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: '' - }, - logging: { - level: 'info', //trace, debug, info, warn, error, fatal - }, - /** - * Options that are displayed to the user/admin in the Polarity integration user-interface. Should be structured - * as an array of option objects. - * - * @type Array - * @optional - */ - "options": [ - { - "key": "apiKey", - "name": "Shodan API Key", - "description": "Your Shodan API Key.", - "default": "", - "type": "text", - "userCanEdit": true, - "adminOnly": false - } - ] + template: { + file: "./templates/shodan-summary.hbs" + } + }, + request: { + // Provide the path to your certFile. Leave an empty string to ignore this option. + // Relative paths are relative to the VT integration's root directory + cert: "", + // Provide the path to your private key. Leave an empty string to ignore this option. + // Relative paths are relative to the VT integration's root directory + key: "", + // Provide the key passphrase if required. Leave an empty string to ignore this option. + // Relative paths are relative to the VT integration's root directory + passphrase: "", + // Provide the Certificate Authority. Leave an empty string to ignore this option. + // Relative paths are relative to the VT integration's root directory + 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: "", + rejectUnauthorized: true + }, + logging: { + level: "info" //trace, debug, info, warn, error, fatal + }, + onDemandOnly: true, + /** + * Options that are displayed to the user/admin in the Polarity integration user-interface. Should be structured + * as an array of option objects. + * + * @type Array + * @optional + */ + options: [ + { + key: "apiKey", + name: "Shodan API Key", + description: "Your Shodan API Key.", + default: "", + type: "password", + userCanEdit: true, + adminOnly: false + } + ] }; diff --git a/integration.js b/integration.js index 6d4165e..c9f4d2f 100644 --- a/integration.js +++ b/integration.js @@ -13,7 +13,6 @@ const limiter = new Bottleneck({ let Logger; let requestWithDefaults; - const IGNORED_IPS = new Set(["127.0.0.1", "255.255.255.255", "0.0.0.0"]); /** @@ -36,11 +35,17 @@ function doLookup(entities, options, cb) { json: true }; - limiter.submit(requestEntity, requestOptions, (err, result) => { + limiter.submit(requestEntity, entity, requestOptions, (err, result) => { requestResults.push([err, result]); if (requestResults.length === entities.length) { - const [errs, results] = rotateResults(results); - if (errs.length) return cb(errs[0]); + const [errs, results] = rotateResults(requestResults); + const errors = errs.filter((i) => i); + + if (errors.length) + return cb({ + err: errors[0], + detail: errors[0].detail || "Error: Something with the Request Failed" + }); const lookupResults = results.map(({ entity, body }) => ({ entity, @@ -57,7 +62,7 @@ function doLookup(entities, options, cb) { }); } -const requestEntity = (requestOptions, callback) => +const requestEntity = (entity, requestOptions, callback) => requestWithDefaults(requestOptions, (err, res, body) => { if (err || typeof res === "undefined") { Logger.error({ err }, "HTTP Request Failed"); From 204dd7b832b41ff9e83c76b7b317aad9e3cbdce5 Mon Sep 17 00:00:00 2001 From: Jon Penwood Date: Wed, 1 Apr 2020 17:26:48 -0500 Subject: [PATCH 03/10] Added Highwater request queue limit and overflow strategy along with error handling. --- integration.js | 81 ++++++----- package-lock.json | 356 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 404 insertions(+), 34 deletions(-) create mode 100644 package-lock.json diff --git a/integration.js b/integration.js index c9f4d2f..68203a5 100644 --- a/integration.js +++ b/integration.js @@ -1,19 +1,22 @@ -"use strict"; +'use strict'; -const request = require("request"); -const config = require("./config/config"); -const fs = require("fs"); -const Bottleneck = require("bottleneck"); +const request = require('request'); +const config = require('./config/config'); +const fs = require('fs'); +const Bottleneck = require('bottleneck'); +const _ = require('lodash'); const limiter = new Bottleneck({ maxConcurrent: 1, + highWater: 15, + strategy: Bottleneck.strategy.OVERFLOW, minTime: 1050 }); let Logger; let requestWithDefaults; -const IGNORED_IPS = new Set(["127.0.0.1", "255.255.255.255", "0.0.0.0"]); +const IGNORED_IPS = new Set(['127.0.0.1', '255.255.255.255', '0.0.0.0']); /** * @@ -23,29 +26,43 @@ const IGNORED_IPS = new Set(["127.0.0.1", "255.255.255.255", "0.0.0.0"]); */ function doLookup(entities, options, cb) { let requestResults = []; - - Logger.trace({ entities }, "doLookup"); + let maxRequestQueueLimitHit = false; + Logger.trace({ entities }, 'doLookup'); entities.forEach((entity) => { if (!entity.isPrivateIP && !IGNORED_IPS.has(entity.value)) { let requestOptions = { - uri: - "https://api.shodan.io/shodan/host/" + entity.value + "?key=" + options.apiKey, - method: "GET", + uri: 'https://api.shodan.io/shodan/host/' + entity.value + '?key=' + options.apiKey, + method: 'GET', json: true }; limiter.submit(requestEntity, entity, requestOptions, (err, result) => { + if (maxRequestQueueLimitHit) return; requestResults.push([err, result]); - if (requestResults.length === entities.length) { + if (_.isEmpty(err) && _.isEmpty(result)) { + maxRequestQueueLimitHit = true; + cb({ + err: { + message: + `Entities: ${entities.map(({ value }) => value).join(', ')} ` + + 'were not able to be searched.' + }, + detail: 'Hit API request Limit. Try again Later' + }); + } + + if (!maxRequestQueueLimitHit && requestResults.length === entities.length) { const [errs, results] = rotateResults(requestResults); - const errors = errs.filter((i) => i); + const errors = errs.filter((err) => !_.isEmpty(err)); - if (errors.length) + if (errors.length) { + Logger.trace({ errors }, 'Something went wrong'); return cb({ err: errors[0], - detail: errors[0].detail || "Error: Something with the Request Failed" + detail: errors[0].detail || 'Error: Something with the Request Failed' }); + } const lookupResults = results.map(({ entity, body }) => ({ entity, @@ -64,15 +81,15 @@ function doLookup(entities, options, cb) { const requestEntity = (entity, requestOptions, callback) => requestWithDefaults(requestOptions, (err, res, body) => { - if (err || typeof res === "undefined") { - Logger.error({ err }, "HTTP Request Failed"); + if (err || typeof res === 'undefined') { + Logger.error({ err }, 'HTTP Request Failed'); return callback({ - detail: "HTTP Request Failed", + detail: 'HTTP Request Failed', err }); } - Logger.trace({ body }, "Result of Lookup"); + Logger.trace({ body }, 'Result of Lookup'); if (res.statusCode === 200) { // we got data! @@ -89,11 +106,11 @@ const requestEntity = (entity, requestOptions, callback) => } else if (res.statusCode === 503) { // reached request limit return callback({ - detail: "Request Limit Reached" + detail: 'Request Limit Reached' }); } else { return callback({ - detail: "Unexpected HTTP Status Received", + detail: 'Unexpected HTTP Status Received', httpStatus: res.statusCode, body }); @@ -113,26 +130,23 @@ function startup(logger) { Logger = logger; let defaults = {}; - if (typeof config.request.cert === "string" && config.request.cert.length > 0) { + if (typeof config.request.cert === 'string' && config.request.cert.length > 0) { defaults.cert = fs.readFileSync(config.request.cert); } - if (typeof config.request.key === "string" && config.request.key.length > 0) { + if (typeof config.request.key === 'string' && config.request.key.length > 0) { defaults.key = fs.readFileSync(config.request.key); } - if ( - typeof config.request.passphrase === "string" && - config.request.passphrase.length > 0 - ) { + if (typeof config.request.passphrase === 'string' && config.request.passphrase.length > 0) { defaults.passphrase = config.request.passphrase; } - if (typeof config.request.ca === "string" && config.request.ca.length > 0) { + if (typeof config.request.ca === 'string' && config.request.ca.length > 0) { defaults.ca = fs.readFileSync(config.request.ca); } - if (typeof config.request.proxy === "string" && config.request.proxy.length > 0) { + if (typeof config.request.proxy === 'string' && config.request.proxy.length > 0) { defaults.proxy = config.request.proxy; } @@ -142,13 +156,12 @@ function startup(logger) { function validateOptions(userOptions, cb) { let errors = []; if ( - typeof userOptions.apiKey.value !== "string" || - (typeof userOptions.apiKey.value === "string" && - userOptions.apiKey.value.length === 0) + typeof userOptions.apiKey.value !== 'string' || + (typeof userOptions.apiKey.value === 'string' && userOptions.apiKey.value.length === 0) ) { errors.push({ - key: "apiKey", - message: "You must provide a Shodan API key" + key: 'apiKey', + message: 'You must provide a Shodan API key' }); } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c82b016 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,356 @@ +{ + "name": "shodan", + "version": "3.0.2-beta", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.0.tgz", + "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "requires": { + "mime-db": "1.43.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "psl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } +} diff --git a/package.json b/package.json index 55eafa6..34e5884 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "private": true, "dependencies": { "bottleneck": "^2.19.5", + "lodash": "^4.17.15", "request": "^2.88" } } From 50f146908492c1ba38c146fc0fe14f94ec618351 Mon Sep 17 00:00:00 2001 From: Jon Penwood Date: Thu, 2 Apr 2020 12:50:27 -0500 Subject: [PATCH 04/10] Added caching for bottleneck instances by api keys, and added rejectedUnauthorized to startup function. --- integration.js | 43 ++++++++++++++++++++++++++----------------- package-lock.json | 5 +++++ package.json | 1 + 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/integration.js b/integration.js index 68203a5..f92a761 100644 --- a/integration.js +++ b/integration.js @@ -1,30 +1,34 @@ 'use strict'; const request = require('request'); -const config = require('./config/config'); const fs = require('fs'); const Bottleneck = require('bottleneck'); const _ = require('lodash'); +var cache = require('memory-cache'); + +const config = require('./config/config'); -const limiter = new Bottleneck({ - maxConcurrent: 1, - highWater: 15, - strategy: Bottleneck.strategy.OVERFLOW, - minTime: 1050 -}); +let bottlneckApiKeyCache = new cache.Cache(); let Logger; let requestWithDefaults; const IGNORED_IPS = new Set(['127.0.0.1', '255.255.255.255', '0.0.0.0']); -/** - * - * @param entities - * @param options - * @param cb - */ function doLookup(entities, options, cb) { + let limiter = bottlneckApiKeyCache.get(options.apiKey); + + if (!limiter) { + limiter = new Bottleneck({ + id: options.apiKey, + maxConcurrent: 1, + highWater: 15, + strategy: Bottleneck.strategy.OVERFLOW, + minTime: 1050 + }); + bottlneckApiKeyCache.put(options.apiKey, limiter); + } + let requestResults = []; let maxRequestQueueLimitHit = false; Logger.trace({ entities }, 'doLookup'); @@ -42,7 +46,7 @@ function doLookup(entities, options, cb) { requestResults.push([err, result]); if (_.isEmpty(err) && _.isEmpty(result)) { maxRequestQueueLimitHit = true; - cb({ + return cb({ err: { message: `Entities: ${entities.map(({ value }) => value).join(', ')} ` + @@ -52,8 +56,8 @@ function doLookup(entities, options, cb) { }); } - if (!maxRequestQueueLimitHit && requestResults.length === entities.length) { - const [errs, results] = rotateResults(requestResults); + if (requestResults.length === entities.length) { + const [errs, results] = transpose2DArray(requestResults); const errors = errs.filter((err) => !_.isEmpty(err)); if (errors.length) { @@ -117,7 +121,8 @@ const requestEntity = (entity, requestOptions, callback) => } }); -const rotateResults = (results) => +const transpose2DArray = (results) => + // [[a,b],[a,b],[a,b]] -> [[a,a,a],[b,b,b]] results.reduce( (agg, [err, result]) => [ [...agg[0], err], @@ -150,6 +155,10 @@ function startup(logger) { defaults.proxy = config.request.proxy; } + if (typeof config.request.rejectUnauthorized === 'boolean') { + requestOptions.rejectUnauthorized = config.request.rejectUnauthorized; + } + requestWithDefaults = request.defaults(defaults); } diff --git a/package-lock.json b/package-lock.json index c82b016..b89fa23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -209,6 +209,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "memory-cache": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", + "integrity": "sha1-eJCwHVLADI68nVM+H46xfjA0hxo=" + }, "mime-db": { "version": "1.43.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", diff --git a/package.json b/package.json index 34e5884..c2ed208 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "bottleneck": "^2.19.5", "lodash": "^4.17.15", + "memory-cache": "^0.2.0", "request": "^2.88" } } From 235cd8a6ad534eb2a088c45afeda32d2fbe4a4fb Mon Sep 17 00:00:00 2001 From: Jon Penwood Date: Thu, 2 Apr 2020 17:13:45 -0500 Subject: [PATCH 05/10] Fixed small typo wit rejectUnauthorized fix. --- integration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration.js b/integration.js index f92a761..5e6cff5 100644 --- a/integration.js +++ b/integration.js @@ -156,7 +156,7 @@ function startup(logger) { } if (typeof config.request.rejectUnauthorized === 'boolean') { - requestOptions.rejectUnauthorized = config.request.rejectUnauthorized; + defaults.rejectUnauthorized = config.request.rejectUnauthorized; } requestWithDefaults = request.defaults(defaults); From f07d584130433b2474c9fdc8b75d3455bf02a636 Mon Sep 17 00:00:00 2001 From: Ed Date: Tue, 7 Apr 2020 23:23:26 -0400 Subject: [PATCH 06/10] remove `var` for `const` --- integration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration.js b/integration.js index 5e6cff5..fee3604 100644 --- a/integration.js +++ b/integration.js @@ -4,7 +4,7 @@ const request = require('request'); const fs = require('fs'); const Bottleneck = require('bottleneck'); const _ = require('lodash'); -var cache = require('memory-cache'); +const cache = require('memory-cache'); const config = require('./config/config'); From f60661c54642c2a0f29b71a14baeb1d51d7e16fe Mon Sep 17 00:00:00 2001 From: Ed Date: Tue, 7 Apr 2020 23:23:41 -0400 Subject: [PATCH 07/10] Update template to focus on most useful information --- components/shodan-block.js | 13 +- components/shodan-summary.js | 77 ++++---- styles/shodan.less | 99 +++++++--- templates/shodan-block.hbs | 342 ++++++++++++++++++++--------------- 4 files changed, 326 insertions(+), 205 deletions(-) diff --git a/components/shodan-block.js b/components/shodan-block.js index d095095..e6dfb20 100644 --- a/components/shodan-block.js +++ b/components/shodan-block.js @@ -1,4 +1,9 @@ -polarity.export = PolarityComponent.extend({ - details: Ember.computed.alias('block.data.details'), - -}); +polarity.export = PolarityComponent.extend({ + details: Ember.computed.alias('block.data.details'), + showDetails: false, + actions: { + toggleDetails: function(){ + this.toggleProperty('showDetails'); + } + } +}); diff --git a/components/shodan-summary.js b/components/shodan-summary.js index b2f2270..f2efb23 100644 --- a/components/shodan-summary.js +++ b/components/shodan-summary.js @@ -1,35 +1,42 @@ -'use strict'; - -polarity.export = PolarityComponent.extend({ - details: Ember.computed.alias('block.data.details'), - summaryTags: Ember.computed('details.tags', function(){ - let summaryTags = []; - - if(this.get('details.city')){ - summaryTags.push(this.get('details.city')); - } - - if(this.get('details.isp')){ - summaryTags.push(this.get('details.isp')); - } - - if(this.get('details.country_name')){ - summaryTags.push(this.get('details.country_name')); - } - - if(this.get('details.org')){ - summaryTags.push(this.get('details.org')); - } - - let tags = this.get('details.tags'); - if(Array.isArray(tags)){ - summaryTags = summaryTags.concat(tags); - } - - if(summaryTags.length === 0){ - summaryTags.push('No Tags'); - } - return summaryTags; - }) -}); - +'use strict'; + +polarity.export = PolarityComponent.extend({ + details: Ember.computed.alias('block.data.details'), + summaryTags: Ember.computed('details.tags', function() { + let summaryTags = []; + + let location = ''; + const country = this.get('details.country_name'); + const city = this.get('details.city'); + + if (city && country) { + location = `${city}, ${country}`; + } else if (city) { + location = city; + } else if (country) { + location = country; + } + + if (location) { + summaryTags.push(location); + } + + if (this.get('details.isp')) { + summaryTags.push(`ISP: ${this.get('details.isp')}`); + } + + if (this.get('details.org')) { + summaryTags.push(`Org: ${this.get('details.org')}`); + } + + let tags = this.get('details.tags'); + if (Array.isArray(tags)) { + summaryTags = summaryTags.concat(tags); + } + + if (summaryTags.length === 0) { + summaryTags.push('No Tags'); + } + return summaryTags; + }) +}); diff --git a/styles/shodan.less b/styles/shodan.less index 3bd8d61..e2d9738 100644 --- a/styles/shodan.less +++ b/styles/shodan.less @@ -1,22 +1,77 @@ -.section-tag { - background-color: #dedede; - border-radius: 4px; - margin-bottom: 2px; - padding: 3px 3px 2px 3px; - display: inline-block; -} - -pre { - overflow-y: scroll; - overflow-x: hidden; - height: 100px; - padding: 5px; - margin-top: 5px; - resize: vertical; -} - -.p-title{ - a{ - margin-left: 3px; - } -} +.section-tag { + background-color: #dedede; + border-radius: 4px; + margin-bottom: 2px; + padding: 3px 3px 2px 3px; + display: inline-block; +} + +.external-icon{ + font-size: 8px; + margin: 0px 0px 1px 3px; +} + +.data-block{ + border-left: 1px solid #eee; + padding-left: 5px; + margin-left: 0px; +} +div.service-pill{ + display: inline-block; + line-height: 2em; + + span.port{ + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + border-left: 1px solid gray; + border-top: 1px solid gray; + border-bottom: 1px solid gray; + padding: 1px 3px; + background-color: #fff; + margin: 0px; + } + + span.transport{ + border-top: 1px solid gray; + border-bottom: 1px solid gray; + border-left: 1px solid gray; + border-right: 1px solid gray; + padding: 1px 3px; + margin: 0px; + background-color: lighten(#ff915f, 20%); + } + + span.service{ + padding: 1px 3px; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + border-right: 1px solid gray; + border-top: 1px solid gray; + border-bottom: 1px solid gray; + margin: 0px; + background-color: #ddd; + } +} + + +pre { + overflow-y: scroll; + overflow-x: auto; + height: 100px; + padding: 5px; + margin-top: 5px; + resize: vertical; + background-color: #eee; +} + +.p-title{ + a{ + margin-left: 3px; + } +} + +::-webkit-scrollbar { + width: 5px; + height: 5px; + background-color: rgba(0, 0, 0, 0); +} \ No newline at end of file diff --git a/templates/shodan-block.hbs b/templates/shodan-block.hbs index 93e81f7..e1ff168 100644 --- a/templates/shodan-block.hbs +++ b/templates/shodan-block.hbs @@ -1,144 +1,198 @@ -{{#if details.asn}} -
- ASN: - {{details.asn}} -
-{{/if}} - -{{#if details.ports}} - Ports: - {{#each details.ports as |data|}} - - {{/each}} -{{/if}} - -{{#each details.data as |item index|}} - {{#if item.ip_str}} -

- {{fa-icon "info-circle"}} {{item.ip_str}} - (View in Shodan) -

- {{/if}} - - {{#if item.product}} -
- Product: - {{item.product}} -
- {{/if}} - - {{#if item.isp}} -
- ISP: - {{item.isp}} -
- {{/if}} - - {{#if item.org}} -
- Org: - {{item.org}} -
- {{/if}} - - {{#if item.devicetype}} -
- Device Type: - {{item.devicetype}} -
- {{/if}} - - {{#if item.hash}} -
- Hash: - {{item.hash}} -
- {{/if}} - - {{#if item.version}} -
- Version: - {{item.version}} -
- {{/if}} - - {{#if item.os}} -
- OS: - {{item.os}} -
- {{/if}} - - {{#if item.ssh.fingerprint}} -
- SSH Fingerprint: - {{item.ssh.fingerprint}} -
- {{/if}} - - {{#if item.ssh.mac}} -
- SSH MAC: - {{item.ssh.mac}} -
- {{/if}} - - {{#if item.ssh.cipher}} -
- SSH Cipher: - {{item.ssh.cipher}} -
- {{/if}} - - {{#if item.http.robots}} -
- HTTP Robots: -
{{item.http.robots}}
-
- {{/if}} - - {{#if item.http.servers}} -
- HTTP Servers: - {{item.http.servers}} -
- {{/if}} - - {{#if item.hostnames}} -

- {{fa-icon "globe"}} Hostnames -

-
- {{#each item.hostnames as |data|}} - - {{/each}} -
- {{/if}} - - {{#if item.domains}} -

- {{fa-icon "globe"}} Domains -

-
- {{#each item.domains as |data|}} - - {{/each}} -
- {{/if}} - - {{#if item.http.redirects}} -

- {{fa-icon "globe"}} HTTP Redirects -

-
- {{#each item.http.redirects as |data|}} - - {{/each}} -
- {{/if}} - {{#if (not-eq item details.data.lastObject)}} -
- {{/if}} -{{/each}} - - +{{#if details.ip_str}} + View in Shodan {{fa-icon "external-link-square" fixedWidth=true class="external-icon"}} +{{/if}} + +

{{fa-icon "globe"}} Summary

+ +{{#if details.city}} +
+ City: + {{details.city}} +
+{{/if}} + +{{#if details.country_name}} +
+ Country: + {{details.country_name}} +
+{{/if}} + +{{#if details.org}} +
+ Org: + {{details.org}} +
+{{/if}} + +{{#if details.isp}} +
+ ISP: + {{details.isp}} +
+{{/if}} + +{{#if details.last_update}} +
+ Last Update: + {{details.last_update}} +
+{{/if}} + +{{#if details.hostnames}} +
+ Hostnames: + {{join ', ' details.hostnames}} +
+{{/if}} + +{{#if details.asn}} +
+ ASN: + {{details.asn}} +
+{{/if}} + +{{#if details.ports}} +

{{fa-icon "th-large" fixedWidth=true}} Ports

+ {{#each details.ports as |data|}} + + {{/each}} +{{/if}} + +

{{fa-icon "th-list" fixedWidth=true}} Services

+ + +{{#each details.data as |item index|}} +
+ {{item.port}}{{item.transport}}{{item._shodan.module}} +
+{{/each}} + +
+ {{#if showDetails}} + {{fa-icon "caret-down" fixedWidth=true}} Hide Details + {{else}} + {{fa-icon "caret-up" fixedWidth=true}} View Details + {{/if}} +
+ + +{{#if showDetails}} + {{#each details.data as |item index|}} +
+ {{#if item.product}} +

{{fa-icon "box" fixedWidth=true}} {{item.product}}

+ {{else}} +

{{fa-icon "box" fixedWidth=true}} {{item._shodan.module}}

+ {{/if}} +
+ {{item.port}}{{item.transport}}{{item._shodan.module}} +
+ {{#if item.isp}} +
+ ISP: + {{item.isp}} +
+ {{/if}} + + {{#if item.org}} +
+ Org: + {{item.org}} +
+ {{/if}} + + {{#if item.devicetype}} +
+ Device Type: + {{item.devicetype}} +
+ {{/if}} + + {{#if item.hash}} +
+ Hash: + {{item.hash}} +
+ {{/if}} + + {{#if item.version}} +
+ Version: + {{item.version}} +
+ {{/if}} + + {{#if item.os}} +
+ OS: + {{item.os}} +
+ {{/if}} + + {{#if item.ssh.fingerprint}} +
+ SSH Fingerprint: + {{item.ssh.fingerprint}} +
+ {{/if}} + + {{#if item.ssh.mac}} +
+ SSH MAC: + {{item.ssh.mac}} +
+ {{/if}} + + {{#if item.ssh.cipher}} +
+ SSH Cipher: + {{item.ssh.cipher}} +
+ {{/if}} + + {{#if item.http.servers}} +
+ HTTP Servers: + {{item.http.servers}} +
+ {{/if}} + + {{#if item.hostnames}} +
+ Hostnames: + {{join ', ' item.hostnames}} +
+ {{/if}} + + {{#if item.domains}} +
+ Domains: + {{join ', ' item.domains}} +
+ {{/if}} + + {{#if item.http.redirects}} +
+ Redirects: + {{join ', ' item.http.redirects}} +
+ {{/if}} + + {{#if item.http.robots}} +
+ HTTP Robots: +
{{item.http.robots}}
+
+ {{/if}} + + {{#if item.data}} +
+ Data: +
{{item.data}}
+
+ {{/if}} +
+ {{/each}} +{{/if}} \ No newline at end of file From d76a767eb83f14e3d3159460dbe2fb5b1f5a0925 Mon Sep 17 00:00:00 2001 From: Jon Penwood Date: Tue, 21 Apr 2020 12:20:45 -0500 Subject: [PATCH 08/10] Added try again functionality. --- components/shodan-block.js | 41 +++- integration.js | 117 +++++++----- templates/shodan-block.hbs | 378 ++++++++++++++++++++----------------- 3 files changed, 316 insertions(+), 220 deletions(-) diff --git a/components/shodan-block.js b/components/shodan-block.js index e6dfb20..eff835b 100644 --- a/components/shodan-block.js +++ b/components/shodan-block.js @@ -1,9 +1,38 @@ polarity.export = PolarityComponent.extend({ - details: Ember.computed.alias('block.data.details'), - showDetails: false, - actions: { - toggleDetails: function(){ - this.toggleProperty('showDetails'); - } + data: Ember.computed.alias('block.data'), + details: Ember.computed.alias('block.data.details'), + showDetails: false, + entity: Ember.computed.alias('block.entity'), + message: '', + errorMessage: null, + isRunning: false, + actions: { + toggleDetails: function () { + this.toggleProperty('showDetails'); + }, + tryAgain: function () { + const outerThis = this; + + this.set('message', ''); + this.set('errorMessage', ''); + this.set('isRunning', true); + this.get('block').notifyPropertyChange('data'); + + this.sendIntegrationMessage({ data: { entity: this.entity } }) + .then((newDetails) => { + outerThis.set('message', 'Success!'); + outerThis.set('details', newDetails); + }) + .catch((err) => { + outerThis.set( + 'errorMessage', + `Failed on Retry: ${err.message || err.title || err.description || 'Unknown Reason'}` + ); + }) + .finally(() => { + this.set('isRunning', false); + outerThis.get('block').notifyPropertyChange('data'); + }); } + } }); diff --git a/integration.js b/integration.js index fee3604..7fe5072 100644 --- a/integration.js +++ b/integration.js @@ -30,56 +30,65 @@ function doLookup(entities, options, cb) { } let requestResults = []; - let maxRequestQueueLimitHit = false; Logger.trace({ entities }, 'doLookup'); - entities.forEach((entity) => { - if (!entity.isPrivateIP && !IGNORED_IPS.has(entity.value)) { - let requestOptions = { - uri: 'https://api.shodan.io/shodan/host/' + entity.value + '?key=' + options.apiKey, - method: 'GET', - json: true - }; - - limiter.submit(requestEntity, entity, requestOptions, (err, result) => { - if (maxRequestQueueLimitHit) return; - requestResults.push([err, result]); - if (_.isEmpty(err) && _.isEmpty(result)) { - maxRequestQueueLimitHit = true; + const validEntities = entities.filter( + (entity) => !entity.isPrivateIP && !IGNORED_IPS.has(entity.value) + ); + + validEntities.forEach((entity) => { + let requestOptions = { + uri: 'https://api.shodan.io/shodan/host/' + entity.value + '?key=' + options.apiKey, + method: 'GET', + json: true + }; + + limiter.submit(requestEntity, entity, requestOptions, (err, result) => { + const maxRequestQueueLimitHit = + (_.isEmpty(err) && _.isEmpty(result)) || + (err && err.message === 'This job has been dropped by Bottleneck'); + + requestResults.push([ + err, + maxRequestQueueLimitHit ? { ...result, entity, limitReached: true } : result + ]); + + if (requestResults.length === validEntities.length) { + const [errs, results] = transpose2DArray(requestResults); + const errors = errs.filter( + (err) => + !_.isEmpty(err) && err && err.message === 'This job has been dropped by Bottleneck' + ); + + if (errors.length) { + Logger.trace({ errors }, 'Something went wrong'); return cb({ - err: { - message: - `Entities: ${entities.map(({ value }) => value).join(', ')} ` + - 'were not able to be searched.' - }, - detail: 'Hit API request Limit. Try again Later' + err: errors[0], + detail: errors[0].detail || 'Error: Something with the Request Failed' }); } - if (requestResults.length === entities.length) { - const [errs, results] = transpose2DArray(requestResults); - const errors = errs.filter((err) => !_.isEmpty(err)); - - if (errors.length) { - Logger.trace({ errors }, 'Something went wrong'); - return cb({ - err: errors[0], - detail: errors[0].detail || 'Error: Something with the Request Failed' - }); - } - - const lookupResults = results.map(({ entity, body }) => ({ - entity, - data: body && { - summary: [], - details: body - } - })); - - cb(null, lookupResults); - } - }); - } + const lookupResults = results + .filter((result) => !_.isEmpty(result)) + .map(({ entity, body, limitReached }) => + limitReached + ? { + entity, + isVolatile: true, + data: { details: { limitReached, tags: ['Query Limit Hit'] } } + } + : { + entity, + data: body && { + summary: [], + details: body + } + } + ); + + cb(null, lookupResults); + } + }); }); } @@ -131,6 +140,23 @@ const transpose2DArray = (results) => [[], []] ); +const retryEntity = ({ data: { entity } }, options, callback) => + doLookup([entity], options, (err, lookupResults) => { + if(err) return callback(err); + + const lookupResult = lookupResults[0]; + + if (lookupResult && lookupResult.data && lookupResult.data.details) { + if (details.limitReached) { + callback({ title: 'Hit Query Limit', message: 'Query Limit Still in Effect' }); + } else { + callback(null, details); + } + } else { + callback(null, { noResultsFound: true, tags: ['No Results Found'] }); + } + }); + function startup(logger) { Logger = logger; let defaults = {}; @@ -180,5 +206,6 @@ function validateOptions(userOptions, cb) { module.exports = { doLookup, startup, - validateOptions + validateOptions, + onMessage: retryEntity }; diff --git a/templates/shodan-block.hbs b/templates/shodan-block.hbs index e1ff168..858b3d7 100644 --- a/templates/shodan-block.hbs +++ b/templates/shodan-block.hbs @@ -1,198 +1,238 @@ -{{#if details.ip_str}} - View in Shodan {{fa-icon "external-link-square" fixedWidth=true class="external-icon"}} -{{/if}} - -

{{fa-icon "globe"}} Summary

- -{{#if details.city}} -
- City: - {{details.city}} -
-{{/if}} - -{{#if details.country_name}} -
- Country: - {{details.country_name}} -
-{{/if}} - -{{#if details.org}} -
- Org: - {{details.org}} -
-{{/if}} - -{{#if details.isp}} -
- ISP: - {{details.isp}} -
-{{/if}} - -{{#if details.last_update}} -
- Last Update: - {{details.last_update}} -
-{{/if}} - -{{#if details.hostnames}} -
- Hostnames: - {{join ', ' details.hostnames}} -
-{{/if}} - -{{#if details.asn}} -
- ASN: - {{details.asn}} -
-{{/if}} - -{{#if details.ports}} -

{{fa-icon "th-large" fixedWidth=true}} Ports

- {{#each details.ports as |data|}} - - {{/each}} -{{/if}} - -

{{fa-icon "th-list" fixedWidth=true}} Services

- - -{{#each details.data as |item index|}} -
- {{item.port}}{{item.transport}}{{item._shodan.module}} -
-{{/each}} - -
- {{#if showDetails}} - {{fa-icon "caret-down" fixedWidth=true}} Hide Details - {{else}} - {{fa-icon "caret-up" fixedWidth=true}} View Details - {{/if}} -
- - -{{#if showDetails}} - {{#each details.data as |item index|}} -
- {{#if item.product}} -

{{fa-icon "box" fixedWidth=true}} {{item.product}}

- {{else}} -

{{fa-icon "box" fixedWidth=true}} {{item._shodan.module}}

- {{/if}} -
- {{item.port}}{{item.transport}}{{item._shodan.module}} -
- {{#if item.isp}} +{{#if details.limitReached}} +

{{fa-icon "info-circle"}} Query Limit Reached

+

+ The query for this entity in Shodan did not go through due to the API Query Limit. +

+
- ISP: - {{item.isp}} + {{#if isRunning}} + {{fa-icon "spinner-third" fixedWidth=true spin=true}} Running + {{/if}}
- {{/if}} - - {{#if item.org}} -
- Org: - {{item.org}} + +
+ {{#if message}} +
+ {{message}}
- {{/if}} - - {{#if item.devicetype}} -
- Device Type: - {{item.devicetype}} + {{/if}} + {{#if errorMessage}} +
+ {{errorMessage}}
- {{/if}} - - {{#if item.hash}} + {{/if}} +{{else}} + {{#if details.noResultsFound}} +

{{fa-icon "info-circle"}} No Results Found

+

+ This Entity does not exist in Shodan. +

+ {{else}} + {{#if details.ip_str}} + + View in Shodan {{fa-icon "external-link-square" fixedWidth=true class="external-icon"}} + + {{/if}} + +

{{fa-icon "globe"}} Summary

+ + {{#if details.city}}
- Hash: - {{item.hash}} + City: + {{details.city}}
- {{/if}} + {{/if}} - {{#if item.version}} + {{#if details.country_name}}
- Version: - {{item.version}} + Country: + {{details.country_name}}
- {{/if}} + {{/if}} - {{#if item.os}} + {{#if details.org}}
- OS: - {{item.os}} + Org: + {{details.org}}
- {{/if}} + {{/if}} - {{#if item.ssh.fingerprint}} + {{#if details.isp}}
- SSH Fingerprint: - {{item.ssh.fingerprint}} + ISP: + {{details.isp}}
- {{/if}} + {{/if}} - {{#if item.ssh.mac}} -
- SSH MAC: - {{item.ssh.mac}} -
- {{/if}} + {{#if details.last_update}} +
+ Last Update: + {{details.last_update}} +
+ {{/if}} - {{#if item.ssh.cipher}} + {{#if details.hostnames}}
- SSH Cipher: - {{item.ssh.cipher}} + Hostnames: + {{join ', ' details.hostnames}}
- {{/if}} + {{/if}} - {{#if item.http.servers}} + {{#if details.asn}}
- HTTP Servers: - {{item.http.servers}} + ASN: + {{details.asn}}
- {{/if}} + {{/if}} - {{#if item.hostnames}} -
- Hostnames: - {{join ', ' item.hostnames}} -
- {{/if}} + {{#if details.ports}} +

{{fa-icon "th-large" fixedWidth=true}} Ports

+ {{#each details.ports as |data|}} + + {{/each}} + {{/if}} - {{#if item.domains}} -
- Domains: - {{join ', ' item.domains}} -
- {{/if}} +

{{fa-icon "th-list" fixedWidth=true}} Services

- {{#if item.http.redirects}} -
- Redirects: - {{join ', ' item.http.redirects}} -
- {{/if}} - {{#if item.http.robots}} -
- HTTP Robots: -
{{item.http.robots}}
+ {{#each details.data as |item index|}} +
+ {{item.port}}{{item.transport}}{{item._shodan.module}}
- {{/if}} - - {{#if item.data}} -
- Data: -
{{item.data}}
+ {{/each}} + +
+ {{#if showDetails}} + {{fa-icon "caret-down" fixedWidth=true}} Hide Details + {{else}} + {{fa-icon "caret-up" fixedWidth=true}} View Details + {{/if}}
- {{/if}} -
- {{/each}} + + + {{#if showDetails}} + {{#each details.data as |item index|}} +
+ {{#if item.product}} +

{{fa-icon "box" fixedWidth=true}} {{item.product}}

+ {{else}} +

{{fa-icon "box" fixedWidth=true}} {{item._shodan.module}}

+ {{/if}} +
+ {{item.port}}{{item.transport}}{{item._shodan.module}} +
+ {{#if item.isp}} +
+ ISP: + {{item.isp}} +
+ {{/if}} + + {{#if item.org}} +
+ Org: + {{item.org}} +
+ {{/if}} + + {{#if item.devicetype}} +
+ Device Type: + {{item.devicetype}} +
+ {{/if}} + + {{#if item.hash}} +
+ Hash: + {{item.hash}} +
+ {{/if}} + + {{#if item.version}} +
+ Version: + {{item.version}} +
+ {{/if}} + + {{#if item.os}} +
+ OS: + {{item.os}} +
+ {{/if}} + + {{#if item.ssh.fingerprint}} +
+ SSH Fingerprint: + {{item.ssh.fingerprint}} +
+ {{/if}} + + {{#if item.ssh.mac}} +
+ SSH MAC: + {{item.ssh.mac}} +
+ {{/if}} + + {{#if item.ssh.cipher}} +
+ SSH Cipher: + {{item.ssh.cipher}} +
+ {{/if}} + + {{#if item.http.servers}} +
+ HTTP Servers: + {{item.http.servers}} +
+ {{/if}} + + {{#if item.hostnames}} +
+ Hostnames: + {{join ', ' item.hostnames}} +
+ {{/if}} + + {{#if item.domains}} +
+ Domains: + {{join ', ' item.domains}} +
+ {{/if}} + + {{#if item.http.redirects}} +
+ Redirects: + {{join ', ' item.http.redirects}} +
+ {{/if}} + + {{#if item.http.robots}} +
+ HTTP Robots: +
{{item.http.robots}}
+
+ {{/if}} + + {{#if item.data}} +
+ Data: +
{{item.data}}
+
+ {{/if}} +
+ {{/each}} + {{/if}} + {{/if}} {{/if}} \ No newline at end of file From 7d8d9d645edc23e3192aa6267a122cb286b76318 Mon Sep 17 00:00:00 2001 From: Ed Date: Thu, 30 Apr 2020 13:19:05 -0400 Subject: [PATCH 09/10] Fix retry search, add button to summary tag. --- components/shodan-block.js | 1 - components/shodan-summary.js | 28 +++++++++++++++++++++++++++- integration.js | 12 ++++++------ package.json | 2 +- styles/shodan.less | 13 +++++++++++++ templates/shodan-block.hbs | 8 ++++---- templates/shodan-summary.hbs | 30 +++++++++++++++++------------- 7 files changed, 68 insertions(+), 26 deletions(-) diff --git a/components/shodan-block.js b/components/shodan-block.js index eff835b..358c6c0 100644 --- a/components/shodan-block.js +++ b/components/shodan-block.js @@ -16,7 +16,6 @@ polarity.export = PolarityComponent.extend({ this.set('message', ''); this.set('errorMessage', ''); this.set('isRunning', true); - this.get('block').notifyPropertyChange('data'); this.sendIntegrationMessage({ data: { entity: this.entity } }) .then((newDetails) => { diff --git a/components/shodan-summary.js b/components/shodan-summary.js index f2efb23..c4cda6b 100644 --- a/components/shodan-summary.js +++ b/components/shodan-summary.js @@ -2,6 +2,7 @@ polarity.export = PolarityComponent.extend({ details: Ember.computed.alias('block.data.details'), + entity: Ember.computed.alias('block.entity'), summaryTags: Ember.computed('details.tags', function() { let summaryTags = []; @@ -38,5 +39,30 @@ polarity.export = PolarityComponent.extend({ summaryTags.push('No Tags'); } return summaryTags; - }) + }), + actions: { + tryAgain: function() { + const outerThis = this; + + this.set('message', ''); + this.set('errorMessage', ''); + this.set('isRunning', true); + + this.sendIntegrationMessage({ data: { entity: this.entity } }) + .then((newDetails) => { + outerThis.set('message', 'Success!'); + outerThis.set('details', newDetails); + }) + .catch((err) => { + outerThis.set( + 'errorMessage', + `Failed on Retry: ${err.message || err.title || err.description || 'Unknown Reason'}` + ); + }) + .finally(() => { + this.set('isRunning', false); + outerThis.get('block').notifyPropertyChange('data'); + }); + } + } }); diff --git a/integration.js b/integration.js index 7fe5072..01ab675 100644 --- a/integration.js +++ b/integration.js @@ -75,7 +75,7 @@ function doLookup(entities, options, cb) { ? { entity, isVolatile: true, - data: { details: { limitReached, tags: ['Query Limit Hit'] } } + data: { details: { limitReached, tags: ['Search Limit Reached'] } } } : { entity, @@ -119,7 +119,7 @@ const requestEntity = (entity, requestOptions, callback) => } else if (res.statusCode === 503) { // reached request limit return callback({ - detail: 'Request Limit Reached' + detail: 'Search Limit Reached' }); } else { return callback({ @@ -140,17 +140,17 @@ const transpose2DArray = (results) => [[], []] ); -const retryEntity = ({ data: { entity } }, options, callback) => +const retryEntity = ({ data: { entity } }, options, callback) => doLookup([entity], options, (err, lookupResults) => { if(err) return callback(err); const lookupResult = lookupResults[0]; if (lookupResult && lookupResult.data && lookupResult.data.details) { - if (details.limitReached) { - callback({ title: 'Hit Query Limit', message: 'Query Limit Still in Effect' }); + if (lookupResult.data.details.limitReached) { + callback({ title: 'Search Limit Reached', message: 'Search Limit Still in Effect' }); } else { - callback(null, details); + callback(null, lookupResult.data.details); } } else { callback(null, { noResultsFound: true, tags: ['No Results Found'] }); diff --git a/package.json b/package.json index c2ed208..ef88b4d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "main": "./integration.js", "name": "shodan", - "version": "3.0.2-beta", + "version": "3.0.3-beta", "private": true, "dependencies": { "bottleneck": "^2.19.5", diff --git a/styles/shodan.less b/styles/shodan.less index e2d9738..e9b4031 100644 --- a/styles/shodan.less +++ b/styles/shodan.less @@ -5,6 +5,19 @@ padding: 3px 3px 2px 3px; display: inline-block; } +.summary-try-again-button { + padding: 0px 3px; + background-color: rgba(0,0,0,.3); + border: 0; + color: #fff; + text-transform: lowercase; + font-size: 10px; + border-radius: 2px; +} + +.summary-try-again-button:hover{ + background-color: rgba(0,0,0,.4); +} .external-icon{ font-size: 8px; diff --git a/templates/shodan-block.hbs b/templates/shodan-block.hbs index 858b3d7..04585bc 100644 --- a/templates/shodan-block.hbs +++ b/templates/shodan-block.hbs @@ -1,7 +1,7 @@ {{#if details.limitReached}} -

{{fa-icon "info-circle"}} Query Limit Reached

+

{{fa-icon "info-circle"}} Search Limit Reached

- The query for this entity in Shodan did not go through due to the API Query Limit. + 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.

@@ -14,7 +14,7 @@ class="p-btn btn btn-light mt-1"class="p-btn btn btn-light mt-1" disabled={{isRunning}} > - Try Again? + Try Again
{{#if message}} @@ -35,7 +35,7 @@

{{else}} {{#if details.ip_str}} - + View in Shodan {{fa-icon "external-link-square" fixedWidth=true class="external-icon"}} {{/if}} diff --git a/templates/shodan-summary.hbs b/templates/shodan-summary.hbs index df0f879..1ecfa26 100644 --- a/templates/shodan-summary.hbs +++ b/templates/shodan-summary.hbs @@ -1,13 +1,17 @@ -{{#each summaryTags as |tag|}} - - {{block.acronym}} - {{tag}} - -{{/each}} - - - - - - - +{{#each summaryTags as |tag|}} + + {{block.acronym}} + {{#if (eq tag "Search Limit Reached")}} + {{fa-icon "exclamation"}} {{tag}} + {{else}} + {{tag}} + {{/if}} + +{{/each}} + + + + + + + From 9ccea247670d0798fe51342309edc394be1452ab Mon Sep 17 00:00:00 2001 From: Ed Date: Fri, 8 May 2020 11:27:58 -0400 Subject: [PATCH 10/10] Updated README --- README.md | 50 ++++++++++++++++++++++++--------------------- assets/overlay.png | Bin 0 -> 55470 bytes package.json | 2 +- 3 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 assets/overlay.png diff --git a/README.md b/README.md index b12c25e..f008ef1 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,27 @@ -# Polarity Shodan Integration - -Polarity's Shodan integration gives users access to automated IPv4 and IPv6 lookups against the Shodan Host REST API. - -Please see [https://www.shodan.io/](https://www.shodan.io/) for more information. - -![image](https://user-images.githubusercontent.com/306319/46555899-e36b6400-c8b2-11e8-9d31-5dc3e34559bc.png) - -## Shodan Integration Options - -### Shodan API Key - -Your Shodan API Key. - -## Installation Instructions - -Installation instructions for integrations are provided on the [PolarityIO GitHub Page](https://polarityio.github.io/). - -## Polarity - -Polarity is a memory-augmentation platform that improves and accelerates analyst decision making. For more information about the Polarity platform please see: - -https://polarity.io/ +# Polarity Shodan Integration + +![mode:on demand only](https://img.shields.io/badge/mode-on%20demand%20only-blue.svg) + +Polarity's Shodan integration gives users access to automated IPv4 and IPv6 lookups against the Shodan Host REST API. The Shodan REST API restricts searches to 1 per second. The integration will automatically throttle lookups to stay below this limit and will queue up to 15 search requests per API key. If the queue is full, you will receive back a response indicating that the queue is full and will have the option to rerun the search from the Overlay Window. + +Please see [https://www.shodan.io/](https://www.shodan.io/) for more information. + +| ![image](assets/overlay.png) | +|---| +|*Shodan Example* | + +## Shodan Integration Options + +### Shodan API Key + +Your Shodan API Key. + +## Installation Instructions + +Installation instructions for integrations are provided on the [PolarityIO GitHub Page](https://polarityio.github.io/). + +## Polarity + +Polarity is a memory-augmentation platform that improves and accelerates analyst decision making. For more information about the Polarity platform please see: + +https://polarity.io/ diff --git a/assets/overlay.png b/assets/overlay.png new file mode 100644 index 0000000000000000000000000000000000000000..b8020c80c21e9b82190fc869eedc33804c175a9b GIT binary patch literal 55470 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 QZ;{RuZ9}a}h+V}001i!x!vFvP literal 0 HcmV?d00001 diff --git a/package.json b/package.json index ef88b4d..4e54209 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "main": "./integration.js", "name": "shodan", - "version": "3.0.3-beta", + "version": "3.1.0-beta", "private": true, "dependencies": { "bottleneck": "^2.19.5",