Skip to content

Commit

Permalink
Merge pull request #6 from polarityio/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
penwoodjon authored Apr 29, 2021
2 parents a0b818e + 7b69bbc commit 0f2cdbb
Show file tree
Hide file tree
Showing 6 changed files with 653 additions and 465 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Polarity GreyNoise Integration
The Polarity - GreyNoise integration searches IPs in GreyNoise for internet scan and attack activity related to indicators on your screen. The scan and attack activity is then displayed via the Polarity Overlay Window.
The Polarity - GreyNoise integration searches IPs in GreyNoise for internet scan and attack activity related to indicators on your screen. The scan and attack activity is then displayed via the Polarity Overlay Window, and we support the GreyNoise Community API out of the box if you do not already have an Enterpise API Key.

## IP Address Resuts
<div style="display: flex; justify-content: flex-start; align-items: flex-start;">
Expand All @@ -17,11 +17,11 @@ To learn more about GreyNoise, please visit the [official website](https://greyn

## GreyNoise Integration Options

### GreyNoise Url
URL for Api access to Greynoise
### GreyNoise Enterprise Url
The URL path to the Enterprise GreyNoise API you wish to use

### GreyNoise Api Key
Accounts api key used to access GreyNoise Api.
Accounts api key used to access GreyNoise Api. If no API Key is entered, we will default to using the GreyNoise Community API to search.

### Ignore IPs that have not been seen
If set to true, IPs that have not been seen by Greynoise will not be displayed in the Polarity Overlay window.
Expand Down
7 changes: 4 additions & 3 deletions config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ module.exports = {
options: [
{
key: 'url',
name: 'GreyNoise URL',
description: 'The URL path to the GreyNoise API',
name: 'GreyNoise Enterprise URL',
description: 'The URL path to the Enterprise GreyNoise API you wish to use.',
default: 'https://enterprise.api.greynoise.io/v2',
type: 'text',
userCanEdit: false,
Expand All @@ -102,7 +102,8 @@ module.exports = {
{
key: 'apiKey',
name: 'API Key',
description: 'GreyNoise API Key',
description:
'Accounts api key used to access GreyNoise Api. If no API Key is entered, we will default to using the GreyNoise Community API to search.',
default: '',
type: 'password',
userCanEdit: true,
Expand Down
189 changes: 155 additions & 34 deletions integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,35 +43,56 @@ function startup(logger) {
}

function doLookup(entities, options, cb) {
Logger.trace({ entities });

if(!options.apiKey) {
useGreynoiseCommunityApi(entities, options, cb);
} else {
useGreynoiseStandardApi(entities, options, cb);
}
}

const useGreynoiseCommunityApi = (entities, options, cb) => {
let lookupResults = [];
let tasks = [];

Logger.trace({ entities });

entities.forEach((entity) => {
Logger.trace({ uri: options }, 'Request URI');

tasks.push(function(done) {
if(entity.isIP) {
getIpData(entity, options, done);

} else if (entity.type === 'cve') {
getCveData(entity, options, done)
} else {
done({err: 'Unsupported entity type'})
}
let requestOptions = {
method: 'GET',
uri: 'https://api.greynoise.io/v3/community/' + entity.value,
headers: {
'User-Agent': `greynoise-community-polarity-integration-v${packageVersion}`
},
json: true
};

Logger.trace({ requestOptions }, 'Request Options');

tasks.push(function (done) {
requestWithDefaults(requestOptions, function (error, res, body) {
let processedResult = handleRestError(error, entity, res, body);

if (processedResult.error) {
done(processedResult);
return;
}

done(null, processedResult);
});
});
});

async.parallelLimit(tasks, MAX_PARALLEL_LOOKUPS, (err, results) => {
if (err) return cb(err);
if (err) {
Logger.error({ err: err }, 'Error');
cb(err);
return;
}

results.forEach((result) => {
if (
(result.body === null || (Array.isArray(result.body) && result.body.length === 0)) &&
!(result.riotBody && result.riotBody.riot) &&
!(result.statBody && result.statBody.count > 0)
) {
if (options.maliciousOnly === true && getIsMalicious(result) === false) return;

if (result.body === null || result.body.length === 0) {
lookupResults.push({
entity: result.entity,
data: null
Expand All @@ -81,16 +102,122 @@ function doLookup(entities, options, cb) {
entity: result.entity,
data: {
summary: [],
details: { ...result.body, ...result.riotBody, ...result.statBody }
details: result.body
}
});
}
});

Logger.debug({ lookupResults }, 'Results');
cb(null, lookupResults);
});
};

function handleRestError(error, entity, res, body) {
let result;

if (error) {
return {
error: error,
detail: 'HTTP Request Error'
};
}

if (res.statusCode === 200) {
// we got data!
result = {
entity: entity,
body: body
};
} else if (res.statusCode === 400) {
if (body.message.includes('Request is not a valid routable IPv4 address')) {
result = {
entity: entity,
body: null
};
} else {
result = {
error: 'Bad Request',
detail: body.message
};
}
} else if (res.statusCode === 404) {
// "IP not observed scanning the internet or contained in RIOT data set."
result = {
entity: entity,
body: null
};
} else if (res.statusCode === 429) {
result = {
entity: entity,
body: { limitHit: true }
};
} else {
result = {
error: 'Unexpected Error',
statusCode: res ? res.statusCode : 'Unknown',
detail: 'An unexpected error occurred',
body
};
}

return result;
}

function getIsMalicious(result) {
if (result.body && result.body.classification && result.body.classification === 'malicious') {
return true;
} else {
return false;
}
}

const useGreynoiseStandardApi = (entities, options, cb) => {
let lookupResults = [];
let tasks = [];

entities.forEach((entity) => {
Logger.trace({ uri: options }, 'Request URI');

tasks.push(function (done) {
if (entity.isIP) {
getIpData(entity, options, done);
} else if (entity.type === 'cve') {
getCveData(entity, options, done);
} else {
done({ err: 'Unsupported entity type' });
}
});
});

async.parallelLimit(tasks, MAX_PARALLEL_LOOKUPS, (err, results) => {
if (err) return cb(err);

results.forEach((result) => {
if (
(result.body === null || (Array.isArray(result.body) && result.body.length === 0)) &&
!(result.riotBody && result.riotBody.riot) &&
!(result.statBody && result.statBody.count > 0)
) {
lookupResults.push({
entity: result.entity,
data: null
});
} else {
lookupResults.push({
entity: result.entity,
data: {
summary: [],
details: { ...result.body, ...result.riotBody, ...result.statBody }
}
});
}
});

cb(null, lookupResults);
});
};

const getIpData = (entity, options, done) => {
let noiseContextRequestOptions = {
method: 'GET',
Expand Down Expand Up @@ -312,21 +439,15 @@ const processGnqlStatsRequestResults = (options, res, statBody, gnqlResult, done
};

function validateOptions(userOptions, cb) {
let errors = [];
if (
typeof userOptions.apiKey.value !== 'string' ||
(typeof userOptions.apiKey.value === 'string' && userOptions.apiKey.value.length === 0)
) {
errors.push({
key: 'apiKey',
message: 'You must provide a valid API key'
});
}
cb(null, errors);
const urlError = userOptions.url.value && userOptions.url.value.endsWith('/')
? [{ key: 'url', message: 'Your Url must not end with "/".' }]
: [];

cb(null, urlError);
}

module.exports = {
doLookup: doLookup,
startup: startup,
validateOptions: validateOptions
doLookup,
startup,
validateOptions
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"main": "./integration.js",
"name": "GreyNoise",
"version": "3.1.1-beta",
"version": "3.2.0-beta",
"private": true,
"dependencies": {
"request": "^2.88.2",
Expand Down
Loading

0 comments on commit 0f2cdbb

Please sign in to comment.