From 5afcc8de4f8e9eb9e27e2edf53dd18750779c872 Mon Sep 17 00:00:00 2001 From: Wes Lambert Date: Thu, 21 Apr 2022 16:42:46 +0000 Subject: [PATCH] Add JA3er analyzer and associated test --- .../files/analyzers/ja3er/__init__.py | 0 .../files/analyzers/ja3er/ja3er.json | 7 ++ salt/sensoroni/files/analyzers/ja3er/ja3er.py | 53 +++++++++++++++ .../files/analyzers/ja3er/ja3er.yaml | 1 + .../files/analyzers/ja3er/ja3er_test.py | 65 +++++++++++++++++++ .../files/analyzers/ja3er/requirements.txt | 2 + 6 files changed, 128 insertions(+) create mode 100644 salt/sensoroni/files/analyzers/ja3er/__init__.py create mode 100644 salt/sensoroni/files/analyzers/ja3er/ja3er.json create mode 100755 salt/sensoroni/files/analyzers/ja3er/ja3er.py create mode 100644 salt/sensoroni/files/analyzers/ja3er/ja3er.yaml create mode 100644 salt/sensoroni/files/analyzers/ja3er/ja3er_test.py create mode 100644 salt/sensoroni/files/analyzers/ja3er/requirements.txt diff --git a/salt/sensoroni/files/analyzers/ja3er/__init__.py b/salt/sensoroni/files/analyzers/ja3er/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/salt/sensoroni/files/analyzers/ja3er/ja3er.json b/salt/sensoroni/files/analyzers/ja3er/ja3er.json new file mode 100644 index 000000000..de072d0b7 --- /dev/null +++ b/salt/sensoroni/files/analyzers/ja3er/ja3er.json @@ -0,0 +1,7 @@ +{ + "name": "JA3er Hash Search", + "version": "0.1", + "author": "Security Onion Solutions", + "description": "This analyzer queries JA3er user agents and sightings", + "supportedTypes" : ["ja3"] +} diff --git a/salt/sensoroni/files/analyzers/ja3er/ja3er.py b/salt/sensoroni/files/analyzers/ja3er/ja3er.py new file mode 100755 index 000000000..330a8dd66 --- /dev/null +++ b/salt/sensoroni/files/analyzers/ja3er/ja3er.py @@ -0,0 +1,53 @@ +import json +import os +import requests +import helpers +import argparse + + +def sendReq(conf, meta, hash): + url = conf['base_url'] + hash + response = requests.request('GET', url) + return response.json() + + +def prepareResults(raw): + if "error" in raw: + if "Sorry" in raw["error"]: + status = "ok" + summary = "No results found." + elif "Invalid hash" in raw["error"]: + status = "caution" + summary = "Invalid hash." + else: + status = "caution" + summary = "internal_failure" + else: + status = "info" + summary = "Results found." + results = {'response': raw, 'summary': summary, 'status': status} + return results + + +def analyze(conf, input): + 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 JA3er 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 + "/ja3er.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/ja3er/ja3er.yaml b/salt/sensoroni/files/analyzers/ja3er/ja3er.yaml new file mode 100644 index 000000000..40d6f64dd --- /dev/null +++ b/salt/sensoroni/files/analyzers/ja3er/ja3er.yaml @@ -0,0 +1 @@ +base_url: https://ja3er.com/search/ diff --git a/salt/sensoroni/files/analyzers/ja3er/ja3er_test.py b/salt/sensoroni/files/analyzers/ja3er/ja3er_test.py new file mode 100644 index 000000000..371c93f5a --- /dev/null +++ b/salt/sensoroni/files/analyzers/ja3er/ja3er_test.py @@ -0,0 +1,65 @@ +from io import StringIO +import sys +from unittest.mock import patch, MagicMock +from ja3er import ja3er +import unittest + + +class TestJa3erMethods(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"] + ja3er.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('ja3er.ja3er.analyze', new=MagicMock(return_value=output)) as mock: + sys.argv = ["cmd", "input"] + ja3er.main() + expected = '{"foo": "bar"}\n' + self.assertEqual(mock_stdout.getvalue(), expected) + mock.assert_called_once() + + def test_sendReq(self): + with patch('requests.request', new=MagicMock(return_value=MagicMock())) as mock: + meta = {} + conf = {"base_url": "myurl/"} + hash = "abcd1234" + response = ja3er.sendReq(conf=conf, meta=meta, hash=hash) + mock.assert_called_once_with("GET", "myurl/abcd1234") + self.assertIsNotNone(response) + + def test_prepareResults_none(self): + raw = {"error": "Sorry no values found"} + results = ja3er.prepareResults(raw) + self.assertEqual(results["response"], raw) + self.assertEqual(results["summary"], "No results found.") + self.assertEqual(results["status"], "ok") + + def test_prepareResults_invalidHash(self): + raw = {"error": "Invalid hash"} + results = ja3er.prepareResults(raw) + self.assertEqual(results["response"], raw) + self.assertEqual(results["summary"], "Invalid hash.") + self.assertEqual(results["status"], "caution") + + def test_prepareResults_info(self): + raw = [{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", "Count": 24874, "Last_seen": "2022-04-08 16:18:38"}, {"Comment": "Opera Linux\n\n", "Reported": "2022-04-20 13:32:49"}, {"Comment": "slamslam\n\n", "Reported": "2022-03-30 04:11:52"}, {"Comment": "Brave browser v1.36.122\n\n", "Reported": "2022-03-28 20:26:42"}] + results = ja3er.prepareResults(raw) + self.assertEqual(results["response"], raw) + self.assertEqual(results["summary"], "Results found.") + self.assertEqual(results["status"], "info") + + def test_analyze(self): + output = {"info": "Results found."} + artifactInput = '{"value":"abcd1234","artifactType":"ja3"}' + conf = {"base_url": "myurl/"} + with patch('ja3er.ja3er.sendReq', new=MagicMock(return_value=output)) as mock: + results = ja3er.analyze(conf, artifactInput) + self.assertEqual(results["summary"], "Results found.") + mock.assert_called_once() diff --git a/salt/sensoroni/files/analyzers/ja3er/requirements.txt b/salt/sensoroni/files/analyzers/ja3er/requirements.txt new file mode 100644 index 000000000..a8980057f --- /dev/null +++ b/salt/sensoroni/files/analyzers/ja3er/requirements.txt @@ -0,0 +1,2 @@ +requests>=2.27.1 +pyyaml>=6.0