Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #21

Merged
merged 20 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,13 @@
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
"useTabs": false,
"overrides": [
{
"files": "*.hbs",
"options": {
"singleQuote": false
}
}
]
}
4 changes: 2 additions & 2 deletions config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
158 changes: 103 additions & 55 deletions integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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);
Expand All @@ -36,27 +51,51 @@ 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
]);

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');
Expand All @@ -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) {
Expand Down Expand Up @@ -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.'
});
Expand Down Expand Up @@ -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;
Expand All @@ -250,6 +300,8 @@ const createSummary = (apiResponse) => {
}
}

if (apiResponse.body.totalVuln) tags.push(`Vulnerabilities: ${apiResponse.body.totalVuln}`);

Logger.trace({ tags }, 'final tags');
return tags;
};
Expand Down Expand Up @@ -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
};
20 changes: 20 additions & 0 deletions logging.js
Original file line number Diff line number Diff line change
@@ -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 };
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Loading
Loading