Skip to content
New issue

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

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

Already on GitHub? # to your account

fixes #1758 - greynoise labs analyzer #2210

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from django.db import migrations
from django.db.models.fields.related_descriptors import (
ForwardManyToOneDescriptor,
ForwardOneToOneDescriptor,
ManyToManyDescriptor,
)

plugin = {
"python_module": {
"health_check_schedule": None,
"update_schedule": None,
"module": "greynoise_labs.GreynoiseLabs",
"base_path": "api_app.analyzers_manager.observable_analyzers",
},
"name": "Greynoise_Labs",
"description": "scan an IP against the Greynoise Labs API (requires authentication token obtained from cookies on greynoise website)",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add markdown with link to the service here so it will be displayed in the gui

"disabled": False,
"soft_time_limit": 60,
"routing_key": "default",
"health_check_status": True,
"type": "observable",
"docker_based": False,
"maximum_tlp": "RED",
"observable_supported": ["ip"],
"supported_filetypes": [],
"run_hash": False,
"run_hash_type": "",
"not_supported_filetypes": [],
"model": "analyzers_manager.AnalyzerConfig",
}

params = [
{
"python_module": {
"module": "greynoise_labs.GreynoiseLabs",
"base_path": "api_app.analyzers_manager.observable_analyzers",
},
"name": "auth_token",
"type": "str",
"description": "Authentication token obtained from cookies on greynoise website.",
"is_secret": True,
"required": True,
}
]

values = []


def _get_real_obj(Model, field, value):
def _get_obj(Model, other_model, value):
if isinstance(value, dict):
real_vals = {}
for key, real_val in value.items():
real_vals[key] = _get_real_obj(other_model, key, real_val)
value = other_model.objects.get_or_create(**real_vals)[0]
# it is just the primary key serialized
else:
if isinstance(value, int):
if Model.__name__ == "PluginConfig":
value = other_model.objects.get(name=plugin["name"])
else:
value = other_model.objects.get(pk=value)
else:
value = other_model.objects.get(name=value)
return value

if (
type(getattr(Model, field))
in [ForwardManyToOneDescriptor, ForwardOneToOneDescriptor]
and value
):
other_model = getattr(Model, field).get_queryset().model
value = _get_obj(Model, other_model, value)
elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value:
other_model = getattr(Model, field).rel.model
value = [_get_obj(Model, other_model, val) for val in value]
return value


def _create_object(Model, data):
mtm, no_mtm = {}, {}
for field, value in data.items():
value = _get_real_obj(Model, field, value)
if type(getattr(Model, field)) is ManyToManyDescriptor:
mtm[field] = value
else:
no_mtm[field] = value
try:
o = Model.objects.get(**no_mtm)
except Model.DoesNotExist:
o = Model(**no_mtm)
o.full_clean()
o.save()
for field, value in mtm.items():
attribute = getattr(o, field)
if value is not None:
attribute.set(value)
return False
return True


def migrate(apps, schema_editor):
Parameter = apps.get_model("api_app", "Parameter")
PluginConfig = apps.get_model("api_app", "PluginConfig")
python_path = plugin.pop("model")
Model = apps.get_model(*python_path.split("."))
if not Model.objects.filter(name=plugin["name"]).exists():
exists = _create_object(Model, plugin)
if not exists:
for param in params:
_create_object(Parameter, param)
for value in values:
_create_object(PluginConfig, value)


def reverse_migrate(apps, schema_editor):
python_path = plugin.pop("model")
Model = apps.get_model(*python_path.split("."))
Model.objects.get(name=plugin["name"]).delete()


class Migration(migrations.Migration):
atomic = False
dependencies = [
("api_app", "0061_job_depth_analysis"),
("analyzers_manager", "0070_urlhaus_threatfox_disable_param"),
]

operations = [migrations.RunPython(migrate, reverse_migrate)]
65 changes: 65 additions & 0 deletions api_app/analyzers_manager/observable_analyzers/greynoise_labs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl
# See the file 'LICENSE' for copying permission.

import requests

from api_app.analyzers_manager.classes import ObservableAnalyzer
from api_app.analyzers_manager.exceptions import AnalyzerRunException
from tests.mock_utils import MockUpResponse, if_mock_connections, patch

queries = {
"noiserank": {
"query_string": "query NoiseRank($ip: String) { noiseRank(ip: $ip) \
{ queryInfo { resultsAvailable resultsLimit } ips { ip noise_score \
sensor_pervasiveness country_pervasiveness payload_diversity \
port_diversity request_rate } } }",
"ip_required": True,
},
"topknocks": {
"query_string": "query TopKnocks($ip: String) { topKnocks(ip: $ip) \
{ queryInfo { resultsAvailable resultsLimit } knock { last_crawled \
last_seen source_ip knock_port title favicon_mmh3_32 \
favicon_mmh3_128 jarm ips emails links tor_exit headers apps } } } ",
"ip_required": True,
},
"topc2s": {
"query_string": "query TopC2s { topC2s { queryInfo \
{ resultsAvailable resultsLimit } c2s { source_ip c2_ips \
c2_domains payload hits pervasiveness } } } "
},
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the analyzer is cool. The only problem regarding this analyzer is this type of query that does not support IP addresses anymore.

For this cases, we usually make the analyzer work in a different way. We maintain a local cache of the data extracted from the Greynoise endpoint (a file in the system) and we open it once the analyzer is triggered.

Thanks to the update method, it is possible to define how to update this file and when.
Please check other analyzers like Tor, Maxmind, Feodo Tracker, etc that do something very similar to what I mentioned

Copy link
Contributor Author

@moonpatel moonpatel Mar 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @mlodic , I have added the update method for this class and some modifications in run method. Here is the new output format after changing the run method.

{
  "noiserank": {
    "data": {
      "noiseRank": {
        "queryInfo": { "resultsAvailable": 1, "resultsLimit": 1 },
        "ips": [
          {
            "ip": "20.235.249.22",
            "noise_score": 12,
            "sensor_pervasiveness": "very low",
            "country_pervasiveness": "low",
            "payload_diversity": "very low",
            "port_diversity": "very low",
            "request_rate": "low"
          }
        ]
      }
    }
  },
  "topknocks": {
    "errors": [
      {
        "message": "20.235.249.22 was not found in KnockKnock",
        "path": ["topKnocks"]
      }
    ],
    "data": null
  },
  "topc2s": { "found": true }
}


class GreynoiseLabs(ObservableAnalyzer):
_auth_token: str

def run(self):
result = {}
url = "https://api.labs.greynoise.io/1/query"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self._auth_token}",
}

try:
for key, value in queries.items():
json_body = {"query": value["query_string"]}
if "ip_required" in value and value["ip_required"]:
json_body["variables"] = {"ip": f"{self.observable_name}"}

response = requests.post(headers=headers, json=json_body, url=url)
response.raise_for_status()
result[key] = response.json()
except requests.RequestException as e:
raise AnalyzerRunException(e)

return result

@classmethod
def _monkeypatch(cls):
patches = [
if_mock_connections(
patch("requests.post", return_value=MockUpResponse({}, 200))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please write examples of outputs here in the tests? like the ones that you shared with me

In this way tests would run with a real output and we could also save an example of their reports here.

Please do a mock for every requests you do (2). See feodo tracker as an example

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like one request whose ip is in noiseRank and one which is not in noiseRank, right? @mlodic

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basically here the tests will cycle through the list of mocks that you write and use one of them every time the analyzer would try to do a http request of the chosen method.

So basically one mock for each request that you make, so one for each endpoint in greynoise

)
]
return super()._monkeypatch(patches=patches)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file is not required cause this analyzer requires additional configuration

# See the file 'LICENSE' for copying permission.

from django.db import migrations


def migrate(apps, schema_editor):
playbook_config = apps.get_model("playbooks_manager", "PlaybookConfig")
AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig")
pc = playbook_config.objects.get(name="FREE_TO_USE_ANALYZERS")
pc.analyzers.add(AnalyzerConfig.objects.get(name="Greynoise_Labs").id)
pc.full_clean()
pc.save()


def reverse_migrate(apps, schema_editor):
playbook_config = apps.get_model("playbooks_manager", "PlaybookConfig")
AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig")
pc = playbook_config.objects.get(name="FREE_TO_USE_ANALYZERS")
pc.analyzers.remove(AnalyzerConfig.objects.get(name="Greynoise_Labs").id)
pc.full_clean()
pc.save()


class Migration(migrations.Migration):
dependencies = [
("playbooks_manager", "0028_add_bgp_ranking_to_free_to_use"),
("analyzers_manager", "0071_analyzer_config_greynoise_labs"),
]

operations = [
migrations.RunPython(migrate, reverse_migrate),
]
1 change: 1 addition & 0 deletions docs/source/Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ The following is the list of the available analyzers you can run out-of-the-box.
* `GreedyBear`: scan an IP or a domain against the [GreedyBear](https://greedybear.honeynet.org/) API (requires API key)
* `GreyNoise`: scan an IP against the [Greynoise](https://www.greynoise.io/) API (requires API key)
* `GreyNoiseCommunity`: scan an IP against the [Community Greynoise API](https://www.greynoise.io/) (requires API key))
* `Greynoise_Labs`: scan an IP against the [Greynoise API](https://www.greynoise.io/) (requires authentication token which can be obtained from cookies on Greynoise website after launching the playground from [here](https://api.labs.greynoise.io/))
* `HashLookupServer_Get_Observable`: check if a md5 or sha1 is available in the database of [known file hosted by CIRCL](https://github.com/adulau/hashlookup-server)
* `HoneyDB_Get`: [HoneyDB](https://honeydb.io/) IP lookup service
* `HoneyDB_Scan_Twitter`: scan an IP against HoneyDB.io's Twitter Threat Feed
Expand Down