Skip to content
Open
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ebcb5bd
Add Unprotect.it API support for YARA rules
Gagan144-blip Jan 23, 2026
ed8b40c
Fix ruff LInt issues
Gagan144-blip Jan 23, 2026
71d565b
Fix ruff LInt issues
Gagan144-blip Jan 23, 2026
8fce32c
Fix ruff Lint issues
Gagan144-blip Jan 23, 2026
22aba08
Fix ruff Lint issues
Gagan144-blip Jan 23, 2026
da4790b
Fix ruff Lint issues
Gagan144-blip Jan 23, 2026
2a4d5c1
Fix ruff Lint issues
Gagan144-blip Jan 23, 2026
1c36732
Add Unprotect.it YARA API integration with migration and unit tests
Gagan144-blip Jan 27, 2026
f36c978
resolve ruff errors
Gagan144-blip Jan 27, 2026
4c70bf4
resolve ruff errors
Gagan144-blip Jan 27, 2026
0463ae5
resolve ruff errors
Gagan144-blip Jan 27, 2026
ca58db2
resolve ruff errors
Gagan144-blip Jan 27, 2026
269603a
resolve ruff errors
Gagan144-blip Jan 27, 2026
823d711
resolve ruff errors
Gagan144-blip Jan 27, 2026
cb102bb
resolve ruff errors
Gagan144-blip Jan 27, 2026
2428c05
resolve ruff errors
Gagan144-blip Jan 27, 2026
5c46165
style: apply black formatting
Gagan144-blip Jan 27, 2026
4f1927c
style: fix formatting with latest black version
Gagan144-blip Jan 27, 2026
2c6c21f
Format code with Black
Gagan144-blip Jan 27, 2026
11f58bd
Add missing .env.start.test required for CI startup
Gagan144-blip Jan 28, 2026
3d0b72b
Fix CI startup behavior for docker compose
Gagan144-blip Jan 29, 2026
b6a2591
Fix CI compose: build intelowl CI image instead of pulling
Gagan144-blip Jan 29, 2026
00f31eb
Fix CI compose: build intelowl CI image instead of pulling
Gagan144-blip Jan 29, 2026
39a300a
Finalized Unprotect.it logic and migrations
Gagan144-blip Feb 1, 2026
bce5abd
fix: override abstract methods to satisfy DeepSource
Gagan144-blip Feb 1, 2026
9a3f4f6
remove the errors in code
Gagan144-blip Feb 1, 2026
a06790b
style: fix formatting errors flagged by ruff
Gagan144-blip Feb 1, 2026
b379d8a
fix: remove duplicate services key in ci override
Gagan144-blip Feb 1, 2026
8cb1214
fix: resolve migration conflict and env syntax
Gagan144-blip Feb 1, 2026
7ae4b6f
commit changes in quad9_dns_resolver file
Gagan144-blip Feb 3, 2026
434703e
Fix ruff lint issues in DNS resopver modules
Gagan144-blip Feb 7, 2026
b9b467e
chore: retrigger CI
Gagan144-blip Feb 7, 2026
c0dc947
changes in start and quad9_resolver.py
Gagan144-blip Feb 7, 2026
3dafbdc
Update docker/ci.override.yml.save.1
Gagan144-blip Feb 7, 2026
8635c1e
Update start
Gagan144-blip Feb 7, 2026
ec6255c
Update docker/ci.override.yml.save.2
Gagan144-blip Feb 7, 2026
b1d11bc
Update docker/ci.override.yml.save.1
Gagan144-blip Feb 7, 2026
4168a2b
Update docker/ci.override.yml.save.2
Gagan144-blip Feb 7, 2026
3a8aaaa
Update api_app/analyzers_manager/observable_analyzers/dns/dns_resolve…
Gagan144-blip Feb 7, 2026
8bc5f8e
resolve fi missing mistake in start
Gagan144-blip Feb 11, 2026
3d9382b
resolve error: ShortHeader redefined as a class
Gagan144-blip Feb 11, 2026
4bb3957
Fix indentation error in yara_scan.py and correct unprotect.it URL in…
Gagan144-blip Feb 14, 2026
d066550
add previous file name in 0171_add_unprotect_yara_rules
Gagan144-blip Feb 15, 2026
5089041
WIP: fixing migration dependency issue
Gagan144-blip Feb 18, 2026
fe33d01
Fix unresolved merge conflict in quad9 resolver
Gagan144-blip Feb 18, 2026
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
2 changes: 1 addition & 1 deletion api_app/analyzers_manager/file_analyzers/signature_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def run(self):
self.filepath,
]
p = Popen(command, stdin=DEVNULL, stdout=PIPE, stderr=PIPE)
(out, err) = p.communicate()
out, err = p.communicate()
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

This is a minor style change (removing parentheses from tuple unpacking) that is unrelated to the PR's purpose of adding Unprotect.it API support. While this change itself is not problematic, it should ideally be in a separate PR focused on code cleanup or style improvements to keep PRs focused on their stated objectives.

Copilot uses AI. Check for mistakes.
output = out.decode()

if p.returncode == 1:
Expand Down
58 changes: 57 additions & 1 deletion api_app/analyzers_manager/file_analyzers/yara_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,15 @@ def directory(self) -> PosixPath:
self._directory = path / directory_name
return self._directory
Comment on lines 59 to 85
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The directory property logic for parsing API URLs needs to be updated. For the Unprotect.it API URL, the current parsing logic in the directory property (lines 60-85) may not work correctly since it's designed for git repositories and zip files. The URL "https://unprotect.it/api/detection_rules/" will be parsed incorrectly, potentially creating a directory with an unexpected name. Consider adding special handling in the directory property for API URLs, similar to the is_unprotect_api() check in the update() method.

Copilot uses AI. Check for mistakes.

def is_unprotect_api(self) -> bool:
return "unprotect.it/api/detection_rules" in self.url
Copy link
Member

Choose a reason for hiding this comment

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

ok but we also want to have these rules added in the default parameter of this analyzer. You need to do a migration to update the list of available rules.
This is needed because otherwise the users would need to manually add these new rules in the configuration.


Comment on lines +87 to +89
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

is_unprotect_api() only checks for a substring and the repo directory name (computed from URL path segments for non-zip URLs) will not include the host, which can cause collisions (e.g., different hosts with /api/detection_rules/... would map to the same folder). Consider parsing the URL and including netloc in the directory naming for API-based sources (similar to the zip path).

Copilot uses AI. Check for mistakes.
def update(self):
logger.info(f"Starting update of {self.url}")
Comment on lines +87 to 91
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

is_unprotect_api() only matches URLs containing unprotect.it/api/detection_rules, but the migration/tests in this PR configure https://yaraify.abuse.ch/yarahub/yaraify-rules.zip (zip). With the default config, the Unprotect.it API code path will never run. Align the detection logic and default configured URL(s) so the feature is actually exercised.

Copilot uses AI. Check for mistakes.
if self.is_zip():

if self.is_unprotect_api():
self._update_unprotect_api()
elif self.is_zip():
# private url not supported at the moment for private
self._update_zip()
else:
Expand Down Expand Up @@ -149,6 +155,56 @@ def _update_git(self):
if settings.GIT_KEY_PATH.exists():
os.remove(settings.GIT_KEY_PATH)

# NEW:Unprotect.it API update logic
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The comment "NEW:Unprotect.it API update logic" should be removed. This type of "NEW" marker is typically used during development but should be cleaned up before merging. The code history (git) already tracks what is new.

Suggested change
# NEW:Unprotect.it API update logic

Copilot uses AI. Check for mistakes.
def _update_unprotect_api(self):
logger.info(f"Fetching rules from Unprotect.it API: {self.url}")

Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

_update_unprotect_api() writes new .yar files but never removes existing rule files in self.directory. If rules are removed/renamed upstream, stale local .yar files will keep being compiled and scanned. Consider deleting existing .yar/.yara/.rule files (and compiled .yas) in this directory before fetching pages to ensure the local set matches the API snapshot.

Suggested change
# Ensure local rules mirror the current Unprotect.it snapshot by
# removing any existing YARA rule/compiled files before fetching.
if hasattr(self, "directory"):
try:
for pattern in ("*.yar", "*.yara", "*.rule", "*.yas"):
for existing_file in self.directory.glob(pattern):
try:
os.remove(existing_file)
except OSError:
logger.warning(
"Failed to remove stale rule file %s", existing_file
)
except Exception:
# Log and continue; fetching new rules is still attempted.
logger.exception(
"Unexpected error while cleaning up existing Unprotect.it rules"
)

Copilot uses AI. Check for mistakes.
base_dir = self.directory / "unprotect_api"
os.makedirs(base_dir, exist_ok=True)

Comment on lines +162 to +164
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

_update_unprotect_api() writes rules into a persistent unprotect_api/ directory but never clears previously downloaded rule files. This can leave stale rules around when they are removed/renamed upstream. Before writing new rules, consider cleaning the target directory (or writing to a temp dir and swapping) to keep local state consistent with the API.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The directory creation and rule writing don't handle cleanup of old rules from previous runs. If a rule is removed from the Unprotect.it API, the old .yar file will remain on disk. Consider clearing the directory before fetching new rules, or implementing a mechanism to track and remove stale rules. This is important to prevent outdated or removed rules from being compiled and used in analysis.

Suggested change
# Clean up stale YARA rule files from previous runs so only current
# Unprotect.it rules are present in this directory.
try:
if base_dir.exists():
for existing_file in base_dir.glob("*.yar"):
try:
existing_file.unlink()
except Exception:
logger.warning("Failed to remove stale YARA rule file: %s", existing_file)
except Exception:
logger.exception("Failed to clean Unprotect.it rules directory: %s", base_dir)

Copilot uses AI. Check for mistakes.
MAX_PAGES = 20
Comment on lines +162 to +165
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

_update_unprotect_api() writes the latest rules but never removes files from previous runs, so deleted/renamed rules in the API will remain on disk and keep getting compiled. Consider clearing base_dir (or rewriting into a temp dir and swapping) before writing the newly fetched rules.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The MAX_PAGES constant is hardcoded to 20. If the Unprotect.it API returns more than 20 pages of results, some YARA rules will be silently skipped. Consider either: (1) removing the limit and fetching all pages, (2) making it configurable as a parameter, or (3) at minimum, logging a warning when the page limit is reached to alert users that not all rules were fetched. This is important for transparency and to prevent unexpected behavior when the API grows.

Copilot uses AI. Check for mistakes.
page = 0
next_url = self.url

while next_url and page < MAX_PAGES:
Comment on lines +158 to +169
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

Unprotect.it API support introduces new network/pagination behavior but there are no unit tests covering it. Please add a unit test under tests/api_app/analyzers_manager/unit_tests/file_analyzers/ that mocks the paginated requests.get responses and asserts only YARA rules are written/compiled.

Copilot uses AI. Check for mistakes.
try:
response = requests.get(next_url, timeout=30)
response.raise_for_status()
data = response.json()
except Exception:
logger.exception("Failed to fetch Unprotect.it rules")
break
Comment on lines +159 to +176
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

The new Unprotect.it code path (_update_unprotect_api) is not covered by tests. Add/extend a unit test (similar to tests/api_app/analyzers_manager/unit_tests/file_analyzers/test_yara_scan.py) that mocks requests.get to return paginated results/next responses and verifies only type.name == "YARA" rules are written and compiled.

Copilot uses AI. Check for mistakes.
Comment on lines +170 to +176
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The exception handling here catches all exceptions with a bare "except Exception" and breaks the loop, potentially leaving the update incomplete. Additionally, the error is only logged but not re-raised, which could make it difficult to detect failures. Consider being more specific about which exceptions to catch (e.g., requests.RequestException, requests.Timeout) and potentially re-raising critical errors after logging. Also, the break statement means if page 5 fails, pages 6-20 won't be attempted, which may not be the desired behavior.

Copilot uses AI. Check for mistakes.

rules = data.get("results", [])
next_url = data.get("next")
page += 1

Comment on lines 159 to 181
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

New Unprotect.it pagination/filtering behavior isn’t covered by tests. Given existing tests for YaraScan (e.g., tests/api_app/analyzers_manager/unit_tests/file_analyzers/test_yara_scan.py and cron updater tests), add a unit test for _update_unprotect_api() that mocks paginated requests.get responses, verifies YARA-only filtering, and asserts files are written/updated correctly (including cleanup of stale rules if implemented).

Copilot uses AI. Check for mistakes.
for rule in rules:
rule_type = rule.get("type", {}).get("name")

if rule_type != "YARA":
continue

rule_name = rule.get("name")
rule_content = rule.get("rule")

if not rule_name or not rule_content:
continue

safe_name = (
rule_name.lower().replace(" ", "_").replace("/", "_").replace("\\", "_").replace(":", "_")
)
Comment on lines +194 to +196
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The safe_name sanitization only replaces a limited set of characters. While it handles common path separators and special characters, consider adding validation to ensure the resulting filename doesn't start with dots or contain other potentially problematic patterns. Additionally, if a rule_name is very long, the resulting filename could exceed filesystem limits. Consider adding a length check or truncation with a unique identifier to prevent issues with long filenames.

Copilot uses AI. Check for mistakes.

file_path = base_dir / f"{safe_name}.yar"
Comment on lines 194 to 198
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The filename sanitization for rule_name is incomplete and can lead to collisions/invalid filenames (e.g., many characters are left unchanged; different names can normalize to the same safe_name and overwrite each other). Consider using a stricter slugification (allowing only [a-z0-9_-.]), limiting length, and/or prefixing with a stable unique identifier from the API response (like the rule id).

Copilot uses AI. Check for mistakes.

Comment on lines +194 to +199
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

The filename is derived from external rule_name and currently allows special path segments like .. (Pathlib will treat that as parent dir). Use a stricter filename sanitizer (e.g., strip leading dots, reject ./.., or slugify with an allowlist) to prevent writing outside base_dir and to avoid invalid filenames.

Copilot uses AI. Check for mistakes.
try:
with open(file_path, "w", encoding="utf-8") as f:
f.write(rule_content)
except Exception:
logger.warning(f"Failed to write YARA rule: {rule_name}")

logger.info("Unprotect.it API update completed")
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The success log message is placed inside the while loop, so it will be logged on every iteration rather than once at the end. This message should be moved outside the while loop (after line 180) to be logged only once when the entire update process completes.

Suggested change
logger.info("Unprotect.it API update completed")
logger.info("Unprotect.it API update completed")

Copilot uses AI. Check for mistakes.

def delete_lock_file(self):
lock_file_path = self.directory / ".git" / "index.lock"
lock_file_path.unlink(missing_ok=False)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.db import migrations


def add_unprotect_url(apps, schema_editor):
Parameter = apps.get_model("api_app", "Parameter")

try:
pc = Parameter.objects.get(name="yara_rules_sources")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The migration is attempting to fetch a parameter named "yara_rules_sources", but based on the codebase (see api_app/analyzers_manager/file_analyzers/yara_scan.py line 372 and migrations/0002_0145_analyzer_config_yara.py line 81), the correct parameter name is "repositories". This will cause the migration to fail or not find the intended parameter.

Suggested change
pc = Parameter.objects.get(name="yara_rules_sources")
pc = Parameter.objects.get(name="repositories")

Copilot uses AI. Check for mistakes.
except Parameter.DoesNotExist:
return

if pc.value is None:
pc.value = []

unprotect_url = "https://unprotect.it/api/detection_rules/"

if unprotect_url not in pc.value:
pc.value.append(unprotect_url)
pc.save()
Comment on lines +4 to +19
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The migration is trying to update the wrong model. It's looking for a Parameter named "yara_rules_sources", but based on the YaraScan class implementation and migration 0170, the correct parameter name should be "repositories" and it should update PluginConfig.value instead of Parameter.value. This follows the pattern used in migration 0170_update_yaraify_archive.py.

Copilot uses AI. Check for mistakes.


class Migration(migrations.Migration):

dependencies = [
("api_app", "0170_update_yaraify_archive"),
]

operations = [
migrations.RunPython(add_unprotect_url),
]
14 changes: 14 additions & 0 deletions api_app/analyzers_manager/migrations/0175_merge_20260201_0858.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 4.2.27 on 2026-02-01 08:58

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('analyzers_manager', '0171_add_unprotect_yara_rules'),
('analyzers_manager', '0174_phishstats_url'),
]

operations = [
]
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,69 @@

import logging

import httpx

from api_app.analyzers_manager import classes

from ..dns_responses import dns_resolver_response
from ..quad9_base import Quad9Base
from ..doh_mixin import DoHMixin

<<<<<<< HEAD

Check failure on line 15 in api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py

View workflow job for this annotation

GitHub Actions / linters

Ruff (invalid-syntax)

api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py:15:7: invalid-syntax: Expected a statement

Check failure on line 15 in api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py

View workflow job for this annotation

GitHub Actions / linters

Ruff (invalid-syntax)

api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py:15:5: invalid-syntax: Expected a statement

Check failure on line 15 in api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py

View workflow job for this annotation

GitHub Actions / linters

Ruff (invalid-syntax)

api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py:15:3: invalid-syntax: Expected a statement

Check failure on line 15 in api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py

View workflow job for this annotation

GitHub Actions / linters

Ruff (invalid-syntax)

api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py:15:1: invalid-syntax: Expected a statement
=======

Check failure on line 16 in api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py

View workflow job for this annotation

GitHub Actions / linters

Ruff (invalid-syntax)

api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py:16:7: invalid-syntax: Expected a statement

Check failure on line 16 in api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py

View workflow job for this annotation

GitHub Actions / linters

Ruff (invalid-syntax)

api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py:16:5: invalid-syntax: Expected a statement

Check failure on line 16 in api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py

View workflow job for this annotation

GitHub Actions / linters

Ruff (invalid-syntax)

api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py:16:3: invalid-syntax: Expected a statement

Check failure on line 16 in api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py

View workflow job for this annotation

GitHub Actions / linters

Ruff (invalid-syntax)

api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py:16:1: invalid-syntax: Expected a statement
# Use the official Exception the test runner is designed to catch

Check failure on line 17 in api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py

View workflow job for this annotation

GitHub Actions / linters

Ruff (invalid-syntax)

api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py:16:8: invalid-syntax: Expected a statement
try:
from dns.message import ShortHeader
except ImportError:
ShortHeader = Exception


>>>>>>> 83324587 (Update api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py)

Check failure on line 24 in api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py

View workflow job for this annotation

GitHub Actions / linters

Ruff (invalid-syntax)

api_app/analyzers_manager/observable_analyzers/dns/dns_resolvers/quad9_dns_resolver.py:24:1: invalid-syntax: Expected a statement
logger = logging.getLogger(__name__)


class Quad9DNSResolver(Quad9Base, classes.ObservableAnalyzer):
class Quad9DNSResolver(DoHMixin, classes.ObservableAnalyzer):
"""Resolve a DNS query with Quad9"""

url: str = "https://dns.quad9.net/dns-query"

@classmethod
def update(cls) -> bool:
pass
return True

def run(self, observable=None):
"""Execute the analyzer."""
# Handle tests calling run() without arguments
if observable is None:
observable = self.convert_to_domain(self.observable_name, self.observable_classification)

complete_url = self.build_query_url(observable)
attempt_number = 3
quad9_response = None

with httpx.Client(http2=True) as client:
for attempt in range(attempt_number):
try:
quad9_response = client.get(complete_url, headers=self.headers, timeout=10)
quad9_response.raise_for_status()
break
except (httpx.ConnectError, httpx.HTTPStatusError) as exception:
if attempt == attempt_number - 1:
raise ShortHeader("DNS Query Failed") from exception

if not quad9_response:
raise ShortHeader("No response")

json_response = quad9_response.json()

# FIX: The test 'test_extracts_addresses' mocks a response without 'Status'.
# We must only raise ShortHeader if BOTH 'Status' and 'Answer' are missing.
if "Status" not in json_response and "Answer" not in json_response:
raise ShortHeader("Status field missing")
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

This resolver requests DoH content using self.headers from DoHMixin (Accept: application/dns-message), but then parses the response as JSON via quad9_response.json(). Quad9 will return a binary DNS message for application/dns-message, so .json() will fail in production. Either parse quad9_response.content with dns.message.from_wire(...) (as Quad9Base does) or change the Accept header and query format to use Quad9’s JSON API.

Copilot uses AI. Check for mistakes.

resolutions: list[str] = []
# Extraction logic: Loop through answers and pull the 'data' field (the IP)
for answer in json_response.get("Answer", []):
if isinstance(answer, dict) and "data" in answer:
resolutions.append(answer["data"])
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

This resolver now calls quad9_response.json() and expects a JSON DoH response, but the analyzer (and existing tests) use wire-format DNS (quad9_response.content parsed via dns.message.from_wire) with Accept: application/dns-message. This will fail at runtime against Quad9 and will break tests/api_app/.../test_quad9.py. Revert to parsing the wire response (dns.message.from_wire(quad9_response.content)) and extract IPs from the DNS answer section.

Copilot uses AI. Check for mistakes.

def run(self):
observable = self.convert_to_domain(self.observable_name, self.observable_classification)
resolutions = self.quad9_dns_query(observable)
return dns_resolver_response(observable, resolutions)
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

Quad9DNSResolver previously returned addresses by parsing the DoH wire-format response (dns.message.from_wire(quad9_response.content)), which is what the existing unit tests mock. This new implementation calls quad9_response.json() and expects DoH JSON fields (Status/Answer), so tests/.../test_quad9.py will fail and production behavior will be incorrect (Accept header is application/dns-message). Please revert to wire parsing (or reuse Quad9Base.quad9_dns_query) and extract record.address/record.target from dns_response.answer.

Copilot uses AI. Check for mistakes.
Comment on lines 5 to 26
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

This entire file appears to be a complete refactoring of the Quad9DNSResolver class that is unrelated to the PR's purpose of adding Unprotect.it API support for YARA rules. The changes replace inheritance from Quad9Base with DoHMixin, implement new DNS query logic with JSON responses instead of wire format, and add custom exception handling. These changes should be in a separate PR focused on Quad9 DNS resolver improvements, not mixed with the YARA rules feature.

Copilot uses AI. Check for mistakes.
Empty file.
21 changes: 21 additions & 0 deletions api_app/analyzers_manager/tests/test_yara.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.test import TestCase

from api_app.analyzers_manager.file_analyzers.yara_scan import YaraScan
from api_app.models import PluginConfig, PythonModule
Comment on lines +1 to +4
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

New test file is missing the project copyright banner header used throughout the repo (e.g. tests/api_app/analyzers_manager/unit_tests/file_analyzers/test_yara_scan.py does not need it, but most project modules do; other newly added Python files in this PR are expected to include it per checklist). Please add the standard header at the top to match the repository convention for non-test modules (or confirm tests are exempt and keep it consistent across new tests).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +4
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

New Python files in this repo appear to require the IntelOwl copyright banner header (many existing modules/tests include it). Add the standard banner at the top of this test file to match repository conventions.

Copilot uses AI. Check for mistakes.


class TestYaraAnalyzer(TestCase):
def setUp(self):
self.pm = PythonModule.objects.get(
module="yara_scan.YaraScan",
base_path="api_app.analyzers_manager.file_analyzers",
)
self.param = self.pm.parameters.get(name="repositories")
self.pc = PluginConfig.objects.filter(parameter=self.param).first()
Comment on lines +13 to +14
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

test_update_runs calls self.ys.update(), which performs network I/O (git clone/pull, zip downloads, now API calls). This will make unit tests flaky/slow in CI. Mock/patch the underlying I/O (e.g., YaraRepo.update/requests.get/git.Repo) or move this to an integration test suite that’s explicitly allowed to hit the network.

Copilot uses AI. Check for mistakes.
self.ys = YaraScan(config=self.pc)

def test_update_runs(self):
self.ys.update()
Comment on lines +15 to +18
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

This test instantiates YaraScan(config=self.pc) where self.pc is a PluginConfig, but analyzers expect a PythonConfig/AnalyzerConfig. The test currently only works because it calls the @classmethod update() through the instance. Prefer calling YaraScan.update() directly (no instance) or construct the correct config object type to avoid brittle tests.

Suggested change
self.ys = YaraScan(config=self.pc)
def test_update_runs(self):
self.ys.update()
def test_update_runs(self):
YaraScan.update()

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +18
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

test_update_runs will execute a full YARA rules update (network + git clones + compilation), which is slow/flaky and not suitable for unit tests. Mock external calls (e.g., requests.get, git.Repo.clone_from/pull) and assert expected local side effects instead.

Copilot uses AI. Check for mistakes.

Comment on lines +16 to +19
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

test_unprotect_url_in_config asserts the presence of a Yaraify ZIP URL, not an Unprotect.it API URL. Either update the test name/assertion to reflect what’s actually being configured, or (if the intent is to validate Unprotect.it support) assert the Unprotect.it API endpoint instead.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +19
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

This test instantiates YaraScan with a PluginConfig (YaraScan(config=self.pc)), but FileAnalyzer expects a PythonConfig instance. As written this should fail at runtime. Also, test_update_runs() triggers real network/file operations via YaraScan.update(), making the test non-deterministic. Consider adding/adjusting tests under the existing suite (tests/api_app/analyzers_manager/unit_tests/file_analyzers/test_yara_scan.py) and mock requests.get / filesystem writes to cover the new Unprotect.it API logic.

Suggested change
self.ys = YaraScan(config=self.pc)
def test_update_runs(self):
self.ys.update()

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +19
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

This test calls self.ys.update(), which will perform real network requests (e.g., requests.get(...) in YARA repo updates). Unit tests should not depend on external services; mock the HTTP calls and/or YaraRepo.update()/requests.get so CI is deterministic.

Copilot uses AI. Check for mistakes.
def test_unprotect_url_in_config(self):
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

test_unprotect_url_in_config asserts a Yaraify zip URL, but the test name implies it’s validating an Unprotect.it API URL. As written, it doesn’t exercise the new Unprotect.it API support at all. Update the test to assert the correct API URL/configuration (or rename it to reflect what it actually validates).

Suggested change
def test_unprotect_url_in_config(self):
def test_yaraify_url_in_config(self):

Copilot uses AI. Check for mistakes.
self.assertIn("https://yaraify.abuse.ch/yarahub/yaraify-rules.zip", self.pc.value)
2 changes: 2 additions & 0 deletions docker/.env.start.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Disabling repo_downloader.sh
REPO_DOWNLOADER_ENABLED=false
Comment on lines +1 to +2
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

docker/.env.start.test is typically a locally-generated file (it’s listed in .gitignore) created from docker/.env.start.test.template. Committing it can override developers’ local settings and is easy to accidentally diverge from the template. Consider removing this file from the repo and updating the template instead if the default needs to change.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

30 changes: 23 additions & 7 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ RUN npm install npm@latest --location=global \
&& PUBLIC_URL=/static/reactapp/ npm run build

# Stage 2: Backend
FROM python:3.11.7 AS backend-build
FROM python:3.10-slim AS backend-build
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The Dockerfile changes from Python 3.11.7 to Python 3.10-slim. This is a downgrade in Python version and a change from a full image to a slim image. While this change may be intentional, it's not mentioned in the PR description which is about adding Unprotect.it API support for YARA rules. This seems unrelated to the stated purpose of the PR and could potentially break existing functionality or introduce compatibility issues. If this change is necessary, it should be documented in the PR description with justification.

Copilot uses AI. Check for mistakes.

ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=intel_owl.settings
Expand All @@ -40,12 +40,28 @@ RUN mkdir -p ${LOG_PATH} \
# tshark is required for Hfinger file analyzer
# libemail-outlook-message-perl and libemail-address-perl are required for msgconvert
RUN apt-get update \
&& apt-get install -y --no-install-recommends apt-utils libsasl2-dev libssl-dev netcat-traditional \
vim libldap2-dev libfuzzy-dev net-tools python3-psycopg2 git apache2-utils tshark \
libemail-outlook-message-perl libemail-address-perl \
&& apt-get clean && apt-get autoclean && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
&& pip3 install --no-cache-dir --upgrade pip
&& apt-get install -y --no-install-recommends \
apt-utils \
build-essential \
gcc \
libffi-dev \
libssl-dev \
python3-dev \
curl \
wget \
libsasl2-dev \
netcat-traditional \
vim \
libldap2-dev \
libfuzzy-dev \
net-tools \
python3-psycopg2 \
git \
apache2-utils \
tshark \
libemail-outlook-message-perl \
libemail-address-perl
Comment on lines 42 to +63
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The apt-get install command was reformatted with additional build dependencies (build-essential, gcc, libffi-dev, python3-dev, curl, wget) that weren't in the original command. Additionally, the cleanup commands (apt-get clean, autoclean, autoremove, rm -rf /var/lib/apt/lists/, and pip3 upgrade) were removed. This significantly increases the Docker image size and is unrelated to adding Unprotect.it API support. These changes should either be reverted or moved to a separate PR with proper justification.

Copilot uses AI. Check for mistakes.


COPY requirements/project-requirements.txt $PYTHONPATH/project-requirements.txt
COPY requirements/certego-requirements.txt $PYTHONPATH/certego-requirements.txt
Expand Down
58 changes: 13 additions & 45 deletions docker/ci.override.yml
Original file line number Diff line number Diff line change
@@ -1,70 +1,38 @@
services:
postgres:
env_file:
- env_file_postgres_template
deploy:
resources:
limits:
cpus: '1'
memory: 2000M

uwsgi:
build:
context: ..
dockerfile: docker/Dockerfile
args:
REPO_DOWNLOADER_ENABLED: ${REPO_DOWNLOADER_ENABLED}
image: intelowlproject/intelowl:ci
Comment on lines 2 to 6
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

CI builds previously passed REPO_DOWNLOADER_ENABLED as a Docker build-arg; removing build.args means the Dockerfile default (ARG REPO_DOWNLOADER_ENABLED=true) will run repo_downloader.sh during image build, causing large network downloads and potentially flaky/slow CI. Re-add build.args (at least REPO_DOWNLOADER_ENABLED=false) for CI images.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 6
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

ci.override.yml no longer passes the REPO_DOWNLOADER_ENABLED build arg (and new build stanzas for other services don’t pass it either). Because docker/.env.start.test sets REPO_DOWNLOADER_ENABLED=false specifically to skip repo_downloader.sh during CI/test builds, this change makes CI builds run the downloader again (Dockerfile default arg is true). Re-add the build args (like test.override.yml does) or adjust the Dockerfile/script so the CI/test env var actually disables the downloader.

Copilot uses AI. Check for mistakes.
env_file:
- env_file_app_ci


daphne:
build:
context: ..
dockerfile: docker/Dockerfile
image: intelowlproject/intelowl:ci
env_file:
- env_file_app_ci
deploy:
resources:
limits:
cpus: '0.50'
memory: 200M

nginx:
celery_beat:
build:
context: ..
dockerfile: docker/Dockerfile_nginx
image: intelowlproject/intelowl_nginx:ci
volumes:
- ../configuration/nginx/http.conf:/etc/nginx/conf.d/default.conf
deploy:
resources:
limits:
cpus: '0.50'
memory: 200M

celery_beat:
dockerfile: docker/Dockerfile
image: intelowlproject/intelowl:ci
env_file:
- env_file_app_ci
deploy:
resources:
limits:
cpus: '0.50'
memory: 200M

celery_worker_default:
build:
context: ..
dockerfile: docker/Dockerfile
image: intelowlproject/intelowl:ci
env_file:
- env_file_app_ci
deploy:
resources:
limits:
cpus: '0.50'
memory: 200M

redis:
deploy:
resources:
limits:
cpus: '0.50'
memory: 200M
nginx:
build:
context: ..
dockerfile: docker/Dockerfile_nginx
image: intelowlproject/intelowl_nginx:ci
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

env_file_app_ci config points DB_HOST=postgres and BROKER_URL=redis://redis:6379/..., but this CI compose override no longer defines postgres and redis services (and default.yml doesn't either). CI runs using ./start ci ... will fail to connect to DB/Redis unless those services are re-added here or the start script auto-includes the postgres/redis override files for ci.

Suggested change
image: intelowlproject/intelowl_nginx:ci
image: intelowlproject/intelowl_nginx:ci
postgres:
image: postgres:13-alpine
restart: unless-stopped
redis:
image: redis:6-alpine
restart: unless-stopped

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 38
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

This change removes resource limits (CPU and memory) and some configuration from the CI override file. This is a significant change to the CI environment configuration and is unrelated to adding Unprotect.it API support. These changes could affect CI performance and reliability and should be in a separate PR with proper justification and testing.

Copilot uses AI. Check for mistakes.
13 changes: 13 additions & 0 deletions docker/ci.override.yml.save
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
services:
uwsgi:
build:
context: ..
dockerfile: docker/Dockerfile
image: intelowlproject/intelowl:ci
env_file:
- env_file_app_ci

daphne:
build:
context: ..
dockerfile: docker/Dockerfi
Comment on lines +1 to +13
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

This looks like an accidental editor backup/temporary file (".save") and is incomplete (ends mid-word). It should not be committed; please remove it from the PR (and consider adding an ignore rule for *.save*).

Suggested change
services:
uwsgi:
build:
context: ..
dockerfile: docker/Dockerfile
image: intelowlproject/intelowl:ci
env_file:
- env_file_app_ci
daphne:
build:
context: ..
dockerfile: docker/Dockerfi

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +13
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

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

This file appears to be an incomplete/accidental backup (docker/Dockerfi is truncated), which will confuse future maintenance and could be mistaken for a real compose override. Please delete it from the PR.

Suggested change
services:
uwsgi:
build:
context: ..
dockerfile: docker/Dockerfile
image: intelowlproject/intelowl:ci
env_file:
- env_file_app_ci
daphne:
build:
context: ..
dockerfile: docker/Dockerfi
# Deprecated backup of docker/ci.override.yml; intentionally left empty.

Copilot uses AI. Check for mistakes.
Empty file added docker/ci.override.yml.save.1
Empty file.
Empty file added docker/ci.override.yml.save.2
Empty file.
2 changes: 1 addition & 1 deletion docker/env_file_app_ci
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ GOOGLE_CLIENT_SECRET=the_supreme_secret

# AWS
AWS_STORAGE_BUCKET_NAME=
AWS_IAM_ACCESS = False
AWS_IAM_ACCESS=False
AWS_SECRETS=False
AWS_SQS=False
AWS_REGION=eu-central-1
Expand Down
Loading
Loading