Skip to content
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
57 changes: 47 additions & 10 deletions api_app/analyzers_manager/observable_analyzers/inquest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,34 @@
import requests

from api_app.analyzers_manager.classes import ObservableAnalyzer
from api_app.analyzers_manager.exceptions import (
AnalyzerConfigurationException,
AnalyzerRunException,
)
from api_app.analyzers_manager.exceptions import AnalyzerConfigurationException, AnalyzerRunException
from api_app.choices import Classification

logger = logging.getLogger(__name__)

# Precompiled regex patterns for generic observable type detection
# Email pattern - comprehensive regex supporting TLDs of any length and subdomains
EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")

# Windows Registry key pattern (specific hives like HKEY_LOCAL_MACHINE, HKLM, etc.)
REGISTRY_PATTERN = re.compile(
r"^(?:HKEY_(?:LOCAL_MACHINE|CURRENT_USER|CLASSES_ROOT|USERS|CURRENT_CONFIG)"
r"|HK(?:LM|CU|CR|U|CC))(?:\\|$)",
re.IGNORECASE,
)

# XMP ID pattern (UUID format)
XMPID_PATTERN = re.compile(
r"^[a-fA-F0-9]{8}-"
r"[a-fA-F0-9]{4}-"
r"[a-fA-F0-9]{4}-"
r"[a-fA-F0-9]{4}-"
r"[a-fA-F0-9]{12}$"
)

# Filename pattern - must have an extension, no path separators
FILENAME_PATTERN = re.compile(r"^[\w\-. ]+\.[a-zA-Z0-9]{1,10}$")


class InQuest(ObservableAnalyzer):
url: str = "https://labs.inquest.net"
Expand Down Expand Up @@ -43,12 +63,29 @@ def hash_type(self):
return hash_type

def type_of_generic(self):
if re.match(r"^[\w\.\+\-]+\@[\w]+\.[a-z]{2,3}$", self.observable_name):
type_ = "email"
else:
# TODO: This should be validated more thoroughly
type_ = "filename"
return type_
"""
Determine the type of a generic observable.

Supported types: email, filename, registry, xmpid
"""
if EMAIL_PATTERN.match(self.observable_name):
return "email"

if REGISTRY_PATTERN.match(self.observable_name):
return "registry"

if XMPID_PATTERN.match(self.observable_name):
return "xmpid"

if FILENAME_PATTERN.match(self.observable_name):
return "filename"

# Default to filename with warning for unrecognized patterns
logger.warning(
f"Could not determine type of generic observable: "
f"'{self.observable_name}'. Defaulting to 'filename'."
)
return "filename"

def run(self):
headers = {"Content-Type": "application/json"}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from unittest.mock import patch

from api_app.analyzers_manager.observable_analyzers.inquest import InQuest
from tests.api_app.analyzers_manager.unit_tests.observable_analyzers.base_test_class import (
BaseAnalyzerTest,
)
from tests.api_app.analyzers_manager.unit_tests.observable_analyzers.base_test_class import BaseAnalyzerTest
from tests.mock_utils import MockUpResponse


Expand All @@ -22,3 +20,74 @@ def get_extra_config(cls) -> dict:
"_api_key_name": "Bearer dummy_api_key",
"generic_identifier_mode": "user-defined",
}


class TypeOfGenericTestCase(InQuestTestCase):
"""Tests for the type_of_generic method."""

@classmethod
def get_extra_config(cls) -> dict:
config = super().get_extra_config()
config["generic_identifier_mode"] = "auto"
return config

def setUp(self):
super().setUp()
# Create a mock analyzer config
from api_app.analyzers_manager.models import AnalyzerConfig

config = AnalyzerConfig.objects.filter(python_module=self.analyzer_class.python_module).first()
if not config:
self.skipTest(
"AnalyzerConfig for InQuest is not available; skipping TypeOfGenericTestCase tests."
)
self.analyzer = self._setup_analyzer(config, "generic", "test")

def test_type_of_generic_email_simple(self):
self.analyzer.observable_name = "user@example.com"
self.assertEqual(self.analyzer.type_of_generic(), "email")

def test_type_of_generic_email_with_subdomain(self):
self.analyzer.observable_name = "user.name+tag@sub.domain.info"
self.assertEqual(self.analyzer.type_of_generic(), "email")

def test_type_of_generic_email_long_tld(self):
self.analyzer.observable_name = "test@domain.museum"
self.assertEqual(self.analyzer.type_of_generic(), "email")

def test_type_of_generic_registry_hkey(self):
self.analyzer.observable_name = "HKEY_LOCAL_MACHINE\\Software\\Test"
self.assertEqual(self.analyzer.type_of_generic(), "registry")

def test_type_of_generic_registry_hklm(self):
self.analyzer.observable_name = "HKLM\\Software\\Microsoft"
self.assertEqual(self.analyzer.type_of_generic(), "registry")

def test_type_of_generic_registry_hkcu(self):
self.analyzer.observable_name = "HKCU\\Desktop"
self.assertEqual(self.analyzer.type_of_generic(), "registry")

def test_type_of_generic_xmpid(self):
self.analyzer.observable_name = "550e8400-e29b-41d4-a716-446655440000"
self.assertEqual(self.analyzer.type_of_generic(), "xmpid")

def test_type_of_generic_filename_simple(self):
self.analyzer.observable_name = "malware.exe"
self.assertEqual(self.analyzer.type_of_generic(), "filename")

def test_type_of_generic_filename_with_spaces(self):
self.analyzer.observable_name = "my document.pdf"
self.assertEqual(self.analyzer.type_of_generic(), "filename")

def test_type_of_generic_unknown_defaults_to_filename(self):
self.analyzer.observable_name = "random-text-no-extension"
self.assertEqual(self.analyzer.type_of_generic(), "filename")

@patch("api_app.analyzers_manager.observable_analyzers.inquest.logger.warning")
def test_type_of_generic_unknown_warning(self, mock_warning):
self.analyzer.observable_name = "random-text-no-extension"
self.analyzer.type_of_generic()
mock_warning.assert_called_once_with(
"Could not determine type of generic observable: "
"'random-text-no-extension'. Defaulting to 'filename'."
)
Loading