diff --git a/salt/sensoroni/files/analyzers/greynoise/__init__.py b/salt/sensoroni/files/analyzers/greynoise/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/salt/sensoroni/files/analyzers/greynoise/greynoise.json b/salt/sensoroni/files/analyzers/greynoise/greynoise.json new file mode 100644 index 000000000..76cef3324 --- /dev/null +++ b/salt/sensoroni/files/analyzers/greynoise/greynoise.json @@ -0,0 +1,7 @@ +{ + "name": "Greynoise IP Analyzer", + "version": "0.1", + "author": "Security Onion Solutions", + "description": "This analyzer queries Greynoise for context around an IP address", + "supportedTypes" : ["ip"] +} diff --git a/salt/sensoroni/files/analyzers/greynoise/greynoise.py b/salt/sensoroni/files/analyzers/greynoise/greynoise.py new file mode 100755 index 000000000..223fd366d --- /dev/null +++ b/salt/sensoroni/files/analyzers/greynoise/greynoise.py @@ -0,0 +1,77 @@ +import json +import os +import sys +import requests +import helpers +import argparse + + +def checkConfigRequirements(conf): + if "api_key" not in conf or len(conf['api_key']) == 0: + sys.exit(126) + else: + return True + + +def sendReq(conf, meta, ip): + url = conf['base_url'] + if conf['api_version'] == 'community': + url = url + 'v3/community/' + ip + elif conf['api_version'] == 'investigate' or 'automate': + url = url + 'v2/noise/context/' + ip + response = requests.request('GET', url) + return response.json() + + +def prepareResults(raw): + if "message" in raw: + if "Success" in raw["message"]: + if "classification" in raw: + if "benign" in raw['classification']: + status = "ok" + summary = "harmless" + elif "malicious" in raw['classification']: + status = "threat" + summary = "malicious" + elif "unknown" in raw['classification']: + status = "caution" + summary = "Results found." + elif "IP not observed scanning the internet or contained in RIOT data set." in raw["message"]: + status = "ok" + summary = "no_results" + elif "Request is not a valid routable IPv4 address" in raw["message"]: + status = "caution" + summary = "Invalid IP address." + else: + status = "info" + summary = raw["message"] + else: + status = "caution" + summary = "internal_failure" + results = {'response': raw, 'summary': summary, 'status': status} + return results + + +def analyze(conf, input): + checkConfigRequirements(conf) + meta = helpers.loadMetadata(__file__) + data = helpers.parseArtifact(input) + helpers.checkSupportedType(meta, data["artifactType"]) + response = sendReq(conf, meta, data["value"]) + return prepareResults(response) + + +def main(): + dir = os.path.dirname(os.path.realpath(__file__)) + parser = argparse.ArgumentParser(description='Search Greynoise for a given artifact') + parser.add_argument('artifact', help='the artifact represented in JSON format') + parser.add_argument('-c', '--config', metavar="CONFIG_FILE", default=dir + "/greynoise.yaml", help='optional config file to use instead of the default config file') + + args = parser.parse_args() + if args.artifact: + results = analyze(helpers.loadConfig(args.config), args.artifact) + print(json.dumps(results)) + + +if __name__ == "__main__": + main() diff --git a/salt/sensoroni/files/analyzers/greynoise/greynoise.yaml b/salt/sensoroni/files/analyzers/greynoise/greynoise.yaml new file mode 100644 index 000000000..aee4f961a --- /dev/null +++ b/salt/sensoroni/files/analyzers/greynoise/greynoise.yaml @@ -0,0 +1,3 @@ +base_url: https://api.greynoise.io/ +api_key: "{{ salt['pillar.get']('sensoroni:analyzers:greynoise:api_key', '') }}" +api_version: "{{ salt['pillar.get']('sensoroni:analyzers:greynoise:api_version', 'community') }}" diff --git a/salt/sensoroni/files/analyzers/greynoise/greynoise_test.py b/salt/sensoroni/files/analyzers/greynoise/greynoise_test.py new file mode 100644 index 000000000..ffc96527a --- /dev/null +++ b/salt/sensoroni/files/analyzers/greynoise/greynoise_test.py @@ -0,0 +1,117 @@ +from io import StringIO +import sys +from unittest.mock import patch, MagicMock +from greynoise import greynoise +import unittest + + +class TestGreynoiseMethods(unittest.TestCase): + + def test_main_missing_input(self): + with patch('sys.exit', new=MagicMock()) as sysmock: + with patch('sys.stderr', new=StringIO()) as mock_stderr: + sys.argv = ["cmd"] + greynoise.main() + self.assertEqual(mock_stderr.getvalue(), "usage: cmd [-h] [-c CONFIG_FILE] artifact\ncmd: error: the following arguments are required: artifact\n") + sysmock.assert_called_once_with(2) + + def test_main_success(self): + output = {"foo": "bar"} + with patch('sys.stdout', new=StringIO()) as mock_stdout: + with patch('greynoise.greynoise.analyze', new=MagicMock(return_value=output)) as mock: + sys.argv = ["cmd", "input"] + greynoise.main() + expected = '{"foo": "bar"}\n' + self.assertEqual(mock_stdout.getvalue(), expected) + mock.assert_called_once() + + def test_checkConfigRequirements_not_present(self): + conf = {"not_a_file_path": "blahblah"} + with self.assertRaises(SystemExit) as cm: + greynoise.checkConfigRequirements(conf) + self.assertEqual(cm.exception.code, 126) + + def test_sendReq_community(self): + with patch('requests.request', new=MagicMock(return_value=MagicMock())) as mock: + meta = {} + conf = {"base_url": "https://myurl/", "api_key": "abcd1234", "api_version": "community"} + ip = "192.168.1.1" + response = greynoise.sendReq(conf=conf, meta=meta, ip=ip) + mock.assert_called_once_with("GET", "https://myurl/v3/community/192.168.1.1") + self.assertIsNotNone(response) + + def test_sendReq_investigate(self): + with patch('requests.request', new=MagicMock(return_value=MagicMock())) as mock: + meta = {} + conf = {"base_url": "https://myurl/", "api_key": "abcd1234", "api_version": "investigate"} + ip = "192.168.1.1" + response = greynoise.sendReq(conf=conf, meta=meta, ip=ip) + mock.assert_called_once_with("GET", "https://myurl/v2/noise/context/192.168.1.1") + self.assertIsNotNone(response) + + def test_sendReq_automate(self): + with patch('requests.request', new=MagicMock(return_value=MagicMock())) as mock: + meta = {} + conf = {"base_url": "https://myurl/", "api_key": "abcd1234", "api_version": "automate"} + ip = "192.168.1.1" + response = greynoise.sendReq(conf=conf, meta=meta, ip=ip) + mock.assert_called_once_with("GET", "https://myurl/v2/noise/context/192.168.1.1") + self.assertIsNotNone(response) + + def test_prepareResults_invalidIP(self): + raw = {"message": "Request is not a valid routable IPv4 address"} + results = greynoise.prepareResults(raw) + self.assertEqual(results["response"], raw) + self.assertEqual(results["summary"], "Invalid IP address.") + self.assertEqual(results["status"], "caution") + + def test_prepareResults_not_found(self): + raw = {"ip": "192.190.1.1", "noise": "false", "riot": "false", "message": "IP not observed scanning the internet or contained in RIOT data set."} + results = greynoise.prepareResults(raw) + self.assertEqual(results["response"], raw) + self.assertEqual(results["summary"], "no_results") + self.assertEqual(results["status"], "ok") + + def test_prepareResults_benign(self): + raw = {"ip": "8.8.8.8", "noise": "false", "riot": "true", "classification": "benign", "name": "Google Public DNS", "link": "https://viz.gn.io", "last_seen": "2022-04-26", "message": "Success"} + results = greynoise.prepareResults(raw) + self.assertEqual(results["response"], raw) + self.assertEqual(results["summary"], "harmless") + self.assertEqual(results["status"], "ok") + + def test_prepareResults_malicious(self): + raw = {"ip": "121.142.87.218", "noise": "true", "riot": "false", "classification": "malicious", "name": "unknown", "link": "https://viz.gn.io", "last_seen": "2022-04-26", "message": "Success"} + results = greynoise.prepareResults(raw) + self.assertEqual(results["response"], raw) + self.assertEqual(results["summary"], "malicious") + self.assertEqual(results["status"], "threat") + + def test_prepareResults_unknown(self): + raw = {"ip": "221.4.62.149", "noise": "true", "riot": "false", "classification": "unknown", "name": "unknown", "link": "https://viz.gn.io", "last_seen": "2022-04-26", "message": "Success"} + results = greynoise.prepareResults(raw) + self.assertEqual(results["response"], raw) + self.assertEqual(results["summary"], "Results found.") + self.assertEqual(results["status"], "caution") + + def test_prepareResults_unknown_message(self): + raw = {"message": "unknown"} + results = greynoise.prepareResults(raw) + self.assertEqual(results["response"], raw) + self.assertEqual(results["summary"], "unknown") + self.assertEqual(results["status"], "info") + + def test_prepareResults_error(self): + raw = {} + results = greynoise.prepareResults(raw) + self.assertEqual(results["response"], raw) + self.assertEqual(results["summary"], "internal_failure") + self.assertEqual(results["status"], "caution") + + def test_analyze(self): + output = {"ip": "221.4.62.149", "noise": "true", "riot": "false", "classification": "unknown", "name": "unknown", "link": "https://viz.gn.io", "last_seen": "2022-04-26", "message": "Success"} + artifactInput = '{"value":"221.4.62.149","artifactType":"ip"}' + conf = {"base_url": "myurl/", "api_key": "abcd1234", "api_version": "community"} + with patch('greynoise.greynoise.sendReq', new=MagicMock(return_value=output)) as mock: + results = greynoise.analyze(conf, artifactInput) + self.assertEqual(results["summary"], "Results found.") + mock.assert_called_once() diff --git a/salt/sensoroni/files/analyzers/greynoise/requirements.txt b/salt/sensoroni/files/analyzers/greynoise/requirements.txt new file mode 100644 index 000000000..a8980057f --- /dev/null +++ b/salt/sensoroni/files/analyzers/greynoise/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.27.1 +pyyaml>=6.0 diff --git a/salt/sensoroni/files/analyzers/greynoise/source-packages/PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl b/salt/sensoroni/files/analyzers/greynoise/source-packages/PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl new file mode 100644 index 000000000..1dfb5c2d3 Binary files /dev/null and b/salt/sensoroni/files/analyzers/greynoise/source-packages/PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl differ diff --git a/salt/sensoroni/files/analyzers/greynoise/source-packages/certifi-2021.10.8-py2.py3-none-any.whl b/salt/sensoroni/files/analyzers/greynoise/source-packages/certifi-2021.10.8-py2.py3-none-any.whl new file mode 100644 index 000000000..fbcb86b5f Binary files /dev/null and b/salt/sensoroni/files/analyzers/greynoise/source-packages/certifi-2021.10.8-py2.py3-none-any.whl differ diff --git a/salt/sensoroni/files/analyzers/greynoise/source-packages/charset_normalizer-2.0.12-py3-none-any.whl b/salt/sensoroni/files/analyzers/greynoise/source-packages/charset_normalizer-2.0.12-py3-none-any.whl new file mode 100644 index 000000000..17a2dfbeb Binary files /dev/null and b/salt/sensoroni/files/analyzers/greynoise/source-packages/charset_normalizer-2.0.12-py3-none-any.whl differ diff --git a/salt/sensoroni/files/analyzers/greynoise/source-packages/idna-3.3-py3-none-any.whl b/salt/sensoroni/files/analyzers/greynoise/source-packages/idna-3.3-py3-none-any.whl new file mode 100644 index 000000000..060541bc9 Binary files /dev/null and b/salt/sensoroni/files/analyzers/greynoise/source-packages/idna-3.3-py3-none-any.whl differ diff --git a/salt/sensoroni/files/analyzers/greynoise/source-packages/requests-2.27.1-py2.py3-none-any.whl b/salt/sensoroni/files/analyzers/greynoise/source-packages/requests-2.27.1-py2.py3-none-any.whl new file mode 100644 index 000000000..807fc6110 Binary files /dev/null and b/salt/sensoroni/files/analyzers/greynoise/source-packages/requests-2.27.1-py2.py3-none-any.whl differ diff --git a/salt/sensoroni/files/analyzers/greynoise/source-packages/urllib3-1.26.9-py2.py3-none-any.whl b/salt/sensoroni/files/analyzers/greynoise/source-packages/urllib3-1.26.9-py2.py3-none-any.whl new file mode 100644 index 000000000..5019453dd Binary files /dev/null and b/salt/sensoroni/files/analyzers/greynoise/source-packages/urllib3-1.26.9-py2.py3-none-any.whl differ