Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
987765d
Merge pull request #2849 from intelowlproject/develop
drosetti May 20, 2025
067ab3a
Add update checker management command, settings, and docs
lvb05 Dec 20, 2025
618f293
Format update checker with black and branch update
lvb05 Dec 31, 2025
eef5c15
Fix linting issues and improve update checker command output
lvb05 Jan 8, 2026
7c59a39
Fix ruff linting issues
lvb05 Jan 8, 2026
47ac868
Restore README.md (remove update checker documentation)
lvb05 Jan 10, 2026
0b12155
Fix formatting in update checker management command
lvb05 Jan 12, 2026
f07a054
Add persistent update check status model
lvb05 Jan 13, 2026
21dd0e2
Fix trailing whitespace in UpdateCheckStatus docstring
lvb05 Jan 13, 2026
14c37da
Use persistent state for update checks and add Celery task
lvb05 Jan 15, 2026
bfce6ca
Add admin GUI notification for update availability and formatting
lvb05 Jan 15, 2026
89eabf3
Add IntelOwl update check system with model, migration, scheduler and…
lvb05 Jan 25, 2026
a7f3f40
Merge branch 'develop' into feature/update-checker-2876
lvb05 Jan 25, 2026
95671b2
style: fix import ordering per ruff
lvb05 Jan 25, 2026
7b9fb40
fix: apply black formatting on tasks.py
lvb05 Jan 25, 2026
024d0a2
fix(update-checker): safe lazy imports and robust version handling
lvb05 Jan 25, 2026
ba4e056
fix(update-checker): prevent duplicate notifications and improve vers…
lvb05 Jan 25, 2026
0c9db74
Restore intel_owl/tasks.py to upstream version
lvb05 Jan 27, 2026
8839e99
Add automated update check system with celery task, model, command an…
lvb05 Jan 27, 2026
471dc63
fix: use __latest__ for django_celery_beat migration dependency
lvb05 Jan 27, 2026
c1681ba
Fix migration 0072: use literal 'days' instead of IntervalSchedule.DA…
lvb05 Jan 27, 2026
82398e2
Fix update-check notifications to work correctly with transactions an…
lvb05 Jan 28, 2026
385ef00
Fix update checker tests to use UserEventQuerySet notifications and i…
lvb05 Jan 28, 2026
7e17a87
Merge branch 'develop' into feature/update-checker-2876
lvb05 Jan 30, 2026
bff38e5
Apply Black formatting after rebase
lvb05 Jan 30, 2026
26f956b
Fix ruff formatting issue in commons settings
lvb05 Jan 30, 2026
c570770
Fix ruff formatting
lvb05 Jan 30, 2026
6f43f7e
.
lvb05 Jan 30, 2026
034ea13
.
lvb05 Jan 30, 2026
9bc6ee1
Add system update panel and API endpoint
lvb05 Feb 1, 2026
81a1e98
Fix lint issues and adjust tests
lvb05 Feb 1, 2026
09f9796
Add system update panel UI and fix lint/format issues
lvb05 Feb 2, 2026
eae37cf
Fix Prettier formatting for update checker components
lvb05 Feb 2, 2026
a554277
correct formatting and comments
lvb05 Feb 6, 2026
a8dec4f
system update notification show up on Home
lvb05 Feb 6, 2026
c1364d1
style change in notification
lvb05 Feb 6, 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
12 changes: 12 additions & 0 deletions api_app/core/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from celery import shared_task

from api_app.core.update_checker import check_for_update


@shared_task
def scheduled_update_check():
"""
Periodic task to check for IntelOwl updates.
Intended to be triggered via celery beat.
"""
check_for_update()
161 changes: 161 additions & 0 deletions api_app/core/update_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import logging
from typing import Optional, Tuple

import requests
from django.conf import settings
from django.db import IntegrityError, transaction
from django.utils.timezone import now

from api_app.models import UpdateCheckStatus

logger = logging.getLogger(__name__)

try:
from api_app.user_events_manager.queryset import UserEventQuerySet
except Exception:
UserEventQuerySet = None

try:
from certego_saas_notifications.models import Notification
except Exception:
Notification = None

STORED_TAG_MAX_LEN = UpdateCheckStatus._meta.get_field("latest_version").max_length


def normalize_version(v: str) -> Tuple[int, ...]:
parts: list[int] = []
for x in str(v).split("."):
if x.isdigit():
parts.append(int(x))
else:
break
return tuple(parts)


def fetch_latest_version() -> Tuple[Optional[str], Optional[str]]:
url = getattr(settings, "UPDATE_CHECK_URL", None)
if not url:
return None, "UPDATE_CHECK_URL not configured"

try:
resp = requests.get(url, headers={"User-Agent": "IntelOwl-Update-Checker"}, timeout=5)
except requests.RequestException as exc:
logger.error("Update check HTTP request failed: %s", exc)
return None, "Failed to fetch release information"

try:
resp.raise_for_status()
except requests.RequestException as exc:
logger.error("Update check HTTP error: %s", exc)
return None, "Update server returned an error"

try:
data = resp.json()
except ValueError:
logger.error("Invalid JSON from update server")
return None, "Invalid response from update server"

tag = data.get("tag_name")
if not tag:
logger.warning("Update response missing tag_name")
return None, "Release response missing tag_name"

return str(tag).lstrip("v"), None


def _notify_admins(title: str, message: str) -> None:
try:
if UserEventQuerySet is not None:
UserEventQuerySet.notify_admins(title=title, message=message, persistent=True, severity="warning")
return
if Notification is not None:
Notification.objects.create(
title=title,
description=message,
level="info",
for_admins=True,
)
return
logger.info("No notification backend available; skipping admin notification")
except Exception:
logger.exception("Failed to send admin notification")


def check_for_update() -> Tuple[bool, str]:
current_version = getattr(settings, "INTEL_OWL_VERSION", None)
if not current_version:
return False, "INTEL_OWL_VERSION setting missing"

latest, err = fetch_latest_version()
if err:
return False, err

latest_full = latest or ""
stored_latest = latest_full[:STORED_TAG_MAX_LEN] if latest_full else None
current_str = str(current_version).lstrip("v")

current_v = normalize_version(current_str)
latest_v = normalize_version(latest_full)

try:
with transaction.atomic():
try:
state, _ = UpdateCheckStatus.objects.select_for_update().get_or_create(pk=1)
except IntegrityError:
state = UpdateCheckStatus.objects.select_for_update().get(pk=1)

state.last_checked_at = now()

if not current_v or not latest_v:
if latest_full != current_str:
message = f"Update available: {latest_full} (current: {current_str})"
else:
message = f"IntelOwl version up to date ({current_str})"

state.save(update_fields=["last_checked_at"])
return True, message

if latest_v > current_v:
message = f"New IntelOwl version available: {latest_full} (current: {current_str})"

should_notify = (state.latest_version != stored_latest) or (not state.notified)
if should_notify:
state.latest_version = stored_latest
state.notified = True
state.save(update_fields=["latest_version", "notified", "last_checked_at"])

def _send_notification():
_notify_admins(
"New IntelOwl version available",
f"Version {latest_full} is available (current: {current_str})",
)

if getattr(settings, "TESTING", False):
_send_notification()
else:
transaction.on_commit(_send_notification)

logger.info(
"New IntelOwl version available (notified scheduled): %s (current: %s)",
latest_full,
current_str,
)
else:
state.save(update_fields=["last_checked_at"])

return True, message

if latest_v < current_v:
state.save(update_fields=["last_checked_at"])
return (
True,
f"Local version ahead of release: {current_str} > {latest_full}",
)

state.save(update_fields=["last_checked_at"])
return True, f"IntelOwl version up to date ({current_str})"

except Exception:
logger.exception("Unexpected error during update check (db/transaction)")
return False, "Unexpected error during update check. See logs for details."
24 changes: 24 additions & 0 deletions api_app/management/commands/check_updates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import logging

from django.core.management.base import BaseCommand, CommandError

from api_app.core.update_checker import check_for_update

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = "Check for newer IntelOwl releases"

def handle(self, *args, **options):
try:
success, message = check_for_update()
except Exception:
logger.exception("Unexpected error during update check")
raise CommandError("Unexpected error during update check. See logs for details.")

if not success:
logger.info("Update check failed: %s", message)
raise CommandError(message)

self.stdout.write(self.style.SUCCESS(message))
95 changes: 95 additions & 0 deletions api_app/migrations/0072_update_check_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import json

import django.utils.timezone
from django.db import migrations, models


def create_weekly_update_task(apps, schema_editor):
IntervalSchedule = apps.get_model("django_celery_beat", "IntervalSchedule")
PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask")

schedule, _ = IntervalSchedule.objects.get_or_create(
every=7,
period="days",
)

PeriodicTask.objects.update_or_create(
name="Weekly IntelOwl Update Check",
defaults={
"interval": schedule,
"task": "api_app.core.tasks.scheduled_update_check",
"kwargs": json.dumps({}),
"enabled": True,
},
)


def remove_weekly_update_task(apps, schema_editor):
PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask")
PeriodicTask.objects.filter(name="Weekly IntelOwl Update Check").delete()


class Migration(migrations.Migration):
dependencies = [
("api_app", "0071_delete_last_elastic_report"),
("django_celery_beat", "__latest__"),
]

operations = [
migrations.CreateModel(
name="UpdateCheckStatus",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"latest_version",
models.CharField(
max_length=20,
null=True,
blank=True,
help_text="Latest version detected during update check",
),
),
(
"notified",
models.BooleanField(
default=False,
help_text="Whether notification has already been sent",
),
),
(
"last_checked_at",
models.DateTimeField(
null=True,
blank=True,
help_text="Last time update check ran",
),
),
(
"created_at",
models.DateTimeField(
default=django.utils.timezone.now,
editable=False,
),
),
(
"updated_at",
models.DateTimeField(auto_now=True),
),
],
options={
"verbose_name": "Update check info",
},
),
migrations.RunPython(
create_weekly_update_task,
remove_weekly_update_task,
),
]
33 changes: 33 additions & 0 deletions api_app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,39 @@
logger = logging.getLogger(__name__)


class UpdateCheckStatus(models.Model):
Copy link
Member

Choose a reason for hiding this comment

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

there should also be a migration

Copy link
Member

Choose a reason for hiding this comment

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

this has not been addressed

"""
Stores global state for IntelOwl update checks.
This model is intended to be used as a singleton (accessed via get_or_create(pk=1)).
Ensures that update notifications are emitted only once per version.
"""

latest_version = models.CharField(
max_length=20,
null=True,
blank=True,
help_text="Latest version detected during update check",
)
notified = models.BooleanField(
default=False,
help_text="Whether a notification has already been sent for this version",
)
last_checked_at = models.DateTimeField(
null=True,
blank=True,
help_text="Last time the update check was executed",
)

created_at = models.DateTimeField(default=now, editable=False)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
verbose_name = "Update check info"

def __str__(self) -> str:
return f"Update check info (latest: {self.latest_version or 'unknown'})"


class PythonModule(models.Model):
"""
Represents a Python module model used in the application.
Expand Down
33 changes: 33 additions & 0 deletions api_app/serializers/system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from django.conf import settings
from rest_framework import serializers

from api_app.core.update_checker import normalize_version
from api_app.models import UpdateCheckStatus


class SystemUpdateStatusSerializer(serializers.Serializer):
current_version = serializers.CharField()
latest_version = serializers.CharField(allow_null=True)
update_available = serializers.BooleanField()
last_checked_at = serializers.DateTimeField(allow_null=True)
notified = serializers.BooleanField()

@staticmethod
def from_state(state: UpdateCheckStatus | None):
current_version = str(getattr(settings, "INTEL_OWL_VERSION", "")).lstrip("v")

latest_version = state.latest_version if state else None
last_checked_at = state.last_checked_at if state else None
notified = state.notified if state else False

update_available = False
if latest_version and normalize_version(latest_version) > normalize_version(current_version):
update_available = True

return {
"current_version": current_version,
"latest_version": latest_version,
"update_available": update_available,
"last_checked_at": last_checked_at,
"notified": notified,
}
2 changes: 2 additions & 0 deletions api_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
ask_analysis_availability,
ask_multi_analysis_availability,
plugin_state_viewer,
system_update_check_view,
)

# Routers provide an easy way of automatically determining the URL conf.
Expand All @@ -30,6 +31,7 @@
urlpatterns = [
# standalone endpoints
path("ask_analysis_availability", ask_analysis_availability),
path("system/update-check/", system_update_check_view, name="system-update-check"),
path("ask_multi_analysis_availability", ask_multi_analysis_availability),
path("analyze_file", analyze_file),
path("analyze_multiple_files", analyze_multiple_files, name="analyze_multiple_files"),
Expand Down
Loading
Loading