Skip to content
Open

v6.8.0 #3941

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d6839b8
[GSoC 2026] chatbot: LangChain 1.x migration (create_agent) — Refs #3…
berardifra Jul 3, 2026
2ff9022
[GSoC 2026] chatbot: harden jobs-vs-investigations routing + accept n…
berardifra Jul 7, 2026
bca0ba6
[GSoC 2026] chatbot: keep the Ollama model resident (OLLAMA_KEEP_ALIV…
berardifra Jul 8, 2026
8683e23
[Connectors] Feat: health check error messages in UI. Closes #3864 (#…
sanjib2006 Jul 22, 2026
5ce2cbe
[GSoC 2026] chatbot: default analyze_observable to a curated playbook…
berardifra Aug 3, 2026
2f4d4ed
fix(frontend): pin the test timezone so date assertions are determini…
berardifra Aug 3, 2026
0360c4c
[GSoC 2026] core(datamodel): populate the reconciled verdict for key-…
berardifra Aug 3, 2026
28e57a4
[GSoC 2026] chatbot: report IntelOwl's own verdict on a job (#3898)
berardifra Aug 3, 2026
37c06a0
fix(exiftool): download from SourceForge, the tarball moved off exift…
shariqueahmad108-ship-it Aug 5, 2026
e39efdb
[Connectors] Introduce CTIConnector to standardize CTI connectors and…
sanjib2006 Aug 6, 2026
3be629c
[Health Check] Refactor health_check methods to return a tuple (statu…
sanjib2006 Aug 8, 2026
14a5079
Fix broken shellcheck script URL in pull_request_automation workflow.…
sahilphad07-sudo Aug 10, 2026
479ac0f
[GSoC 2026] fix(settings): single, env-driven definition for CHATBOT_…
berardifra Aug 11, 2026
d5b4fae
[Connectors] test(slack): Update run method to return notification de…
sanjib2006 Aug 12, 2026
d410ddc
build(deps): bump github/codeql-action from 4.35.1 to 4.37.6 (#3931)
dependabot[bot] Aug 14, 2026
97546d5
[GSoC 2026] chatbot: post-generation guard against fabricated placeho…
berardifra Aug 14, 2026
b146537
Update droidlysis from ce37151 to 914edc5 (#3918)
github-actions[bot] Aug 14, 2026
4c0bf80
build(deps): bump playwright in /integrations/phishing_analyzers (#3900)
dependabot[bot] Aug 14, 2026
2e83201
build(deps): bump actions/stale from 9 to 11 (#3899)
dependabot[bot] Aug 14, 2026
549d632
Update GoReSym from v3.3 to v3.4 (#3847)
github-actions[bot] Aug 14, 2026
781a4a9
Update box-js from 1.9.27 to 1.9.29 (#3846)
github-actions[bot] Aug 14, 2026
553281a
[Connectors] Feature: Add MISP as an optional container
sanjib2006 Aug 14, 2026
33396aa
fix: prevent information exposure in plugin health check errors
sanjib2006 Aug 14, 2026
4e1d01a
Merge pull request #3938 from intelowlproject/gsoc-2026/post-mid-conn…
mlodic Aug 16, 2026
80e9c0d
changelog and bump
mlodic Aug 16, 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
5 changes: 5 additions & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

[**Upgrade Guide**](https://intelowlproject.github.io/docs/IntelOwl/installation/#update-to-the-most-recent-version)

## [v6.8.0](https://github.com/intelowlproject/IntelOwl/releases/tag/v6.8.0)
This release merges all the developments performed by our Google Summer of Code contributors during the last month:
* [Francesco Berardi](https://github.com/berardifra): ["Integrating a Self-Deployed LLM Chatbot for Threat Intelligence"](https://intelowlproject.github.io/blogs/gsoc26_llm_chatbot)
* [Sanjib Behera](https://github.com/sanjib2006): "Integration Ecosystem & Connector Optimization"

## [v6.7.0](https://github.com/intelowlproject/IntelOwl/releases/tag/v6.7.0)
We welcome the first working AI-based integration in IntelOwl! :tada:

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4.35.1
uses: github/codeql-action/init@v4.37.6
with:
languages: python
# Override the default behavior so that the action doesn't attempt
Expand Down Expand Up @@ -93,4 +93,4 @@ jobs:
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.35.1
uses: github/codeql-action/analyze@v4.37.6
9 changes: 1 addition & 8 deletions .github/workflows/dependency_review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,4 @@ jobs:
- name: 'Checkout Repository'
uses: actions/checkout@v6.0.2
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
with:
# GHSA-gr75-jv2w-4656: LangChain path traversal in file-search middleware/loaders.
# Patched only in langchain 1.3.9 (breaking major upgrade). The chatbot uses no
# document loaders, file-search middleware, or hub prompt pulls (only langchain.agents,
# langchain_core.*, langchain_ollama), so the vulnerable code paths are unreachable.
# Accepted until the planned langchain 1.x migration. Approved by @mlodic.
allow-ghsas: GHSA-gr75-jv2w-4656
uses: actions/dependency-review-action@v4
2 changes: 1 addition & 1 deletion .github/workflows/pull_request_automation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
ruff format . --check --diff

- name: Perform ShellCheck Analysis
run: bash <(curl -s https://raw.githubusercontent.com/CICDToolbox/shellcheck/master/pipeline.sh)
run: bash <(curl -s https://raw.githubusercontent.com/lupaxa-cicd-toolbox/shellcheck/master/src/pipeline.sh)

backend-tests:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v11
with:
# PRs
stale-pr-message: "This pull request has been marked as stale because it has had no activity for 10 days. If you are still working on this, please provide some updates or it will be closed in 5 days."
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ docker/env_file_app
docker/env_file_postgres
docker/env_file_integrations
docker/env_file_elasticsearch
docker/env_file_misp
docker/custom.override.yml
venv/
intel_owl_test_env/
Expand Down
8 changes: 3 additions & 5 deletions api_app/analyzers_manager/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,15 +452,13 @@ def _docker_get(self):
raise AssertionError
return resp

def health_check(self, user: User = None) -> bool:
def health_check(self, user: User = None) -> Tuple[bool, str]:
"""
basic health check: if instance is up or not (timeout - 10s)
"""
try:
requests.head(self.url, timeout=10)
except requests.exceptions.RequestException:
health_status = False
return False, "It is NOT up"
else:
health_status = True

return health_status
return True, "It is up and running"
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl
# See the file 'LICENSE' for copying permission.

from django.db import migrations

# Single reviewable reliability table. $-prefixed keys are written as literal
# constants (analyzers_manager/models.py:92-95). Conditionality lives in each
# analyzer's _do_create_data_model gate, so a non-hit produces no data model.
# Reliability tiers reflect source authority: Google Safe Browsing/WebRisk (8)
# and Spamhaus (7) rank above the DNS-resolver blocklists (6).
MAPPINGS = {
"GoogleSafebrowsing": {"$malicious": "evaluation", "$8": "reliability"},
"GoogleWebRisk": {"$malicious": "evaluation", "$8": "reliability"},
"Spamhaus_WQS": {"$malicious": "evaluation", "$7": "reliability"},
"AdGuard": {"$malicious": "evaluation", "$6": "reliability"},
"Quad9_Malicious_Detector": {"$malicious": "evaluation", "$6": "reliability"},
"CloudFlare_Malicious_Detector": {"$malicious": "evaluation", "$6": "reliability"},
"CleanBrowsing_Malicious_Detector": {"$malicious": "evaluation", "$6": "reliability"},
"UltraDNS_Malicious_Detector": {"$malicious": "evaluation", "$6": "reliability"},
"DNS4EU_Malicious_Detector": {"$malicious": "evaluation", "$6": "reliability"},
"Mullvad_DNS": {"$malicious": "evaluation", "$6": "reliability"},
}


def apply_mappings(apps, schema_editor):
AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig")
for name, mapping in MAPPINGS.items():
ac = AnalyzerConfig.objects.filter(name=name).first()
if not ac:
continue
ac.mapping_data_model = mapping
ac.save()


def revert_mappings(apps, schema_editor):
AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig")
for name in MAPPINGS:
ac = AnalyzerConfig.objects.filter(name=name).first()
if not ac:
continue
ac.mapping_data_model = {}
ac.save()


class Migration(migrations.Migration):
dependencies = [
("analyzers_manager", "0194_analyzer_config_rdap"),
]
operations = [
migrations.RunPython(apply_mappings, revert_mappings),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl
# See the file 'LICENSE' for copying permission.

from django.db import migrations

# Same declarative pattern as 0195: $-prefixed keys are written as literal
# constants (analyzers_manager/models.py:92-95). A listing hit is gated in each
# analyzer's _do_create_data_model, so a miss produces no data model.
MAPPINGS = {
"PhishingArmy": {"$malicious": "evaluation", "$6": "reliability"},
"Phishstats": {"$malicious": "evaluation", "$6": "reliability"},
}


def apply_mappings(apps, schema_editor):
AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig")
for name, mapping in MAPPINGS.items():
ac = AnalyzerConfig.objects.filter(name=name).first()
if not ac:
continue
ac.mapping_data_model = mapping
ac.save()


def revert_mappings(apps, schema_editor):
AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig")
for name in MAPPINGS:
ac = AnalyzerConfig.objects.filter(name=name).first()
if not ac:
continue
ac.mapping_data_model = {}
ac.save()


class Migration(migrations.Migration):
dependencies = [
("analyzers_manager", "0195_data_model_key_free_detectors"),
]
operations = [
migrations.RunPython(apply_mappings, revert_mappings),
]
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

from ..dns_responses import malicious_detector_response
from ..doh_mixin import DoHMixin
from .data_model import MaliciousDetectorResponseDataModelMixin

logger = logging.getLogger(__name__)


class AdGuard(DoHMixin, classes.ObservableAnalyzer):
class AdGuard(MaliciousDetectorResponseDataModelMixin, DoHMixin, classes.ObservableAnalyzer):
"""Check if a domain is malicious by AdGuard public resolver."""

url: str = "https://dns.adguard-dns.com/dns-query"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
from api_app.choices import Classification

from ..dns_responses import malicious_detector_response
from .data_model import MaliciousDetectorResponseDataModelMixin


class CleanBrowsingMaliciousDetector(classes.ObservableAnalyzer):
class CleanBrowsingMaliciousDetector(MaliciousDetectorResponseDataModelMixin, classes.ObservableAnalyzer):
"""Resolve a DNS query with CleanBrowsing security endpoint,
Blocked domains return NXDOMAIN with SOA from cleanbrowsing.rpz.noc.org.
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
from api_app.choices import Classification

from ..dns_responses import malicious_detector_response
from .data_model import MaliciousDetectorResponseDataModelMixin


class CloudFlareMaliciousDetector(classes.ObservableAnalyzer):
class CloudFlareMaliciousDetector(MaliciousDetectorResponseDataModelMixin, classes.ObservableAnalyzer):
"""Resolve a DNS query with CloudFlare security endpoint,
if response is 0.0.0.0 the domain in DNS query is malicious.
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl
# See the file 'LICENSE' for copying permission.


class MaliciousDetectorResponseDataModelMixin:
"""Emit a DataModel only on a real malicious hit.

These analyzers map the constant ``$malicious -> evaluation`` in their
``mapping_data_model``, which writes ``evaluation = "malicious"`` unconditionally
whenever a data model is created. So a clean lookup (``malicious: false``), a
timeout, or a failure note would otherwise be stamped MALICIOUS. Gating creation
on ``report["malicious"] is True`` makes a non-hit produce no data model (silent);
it must never map a non-hit to trusted.
"""

def _do_create_data_model(self) -> bool:
return super()._do_create_data_model() and self.report.report.get("malicious") is True
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@

from ..dns4eu_base import DNS4EUBase
from ..dns_responses import malicious_detector_response
from .data_model import MaliciousDetectorResponseDataModelMixin

logger = logging.getLogger(__name__)


class DNS4EUMaliciousDetector(DNS4EUBase):
class DNS4EUMaliciousDetector(MaliciousDetectorResponseDataModelMixin, DNS4EUBase):
url = "https://protective.joindns4.eu/dns-query"

# DNS4EU blocks by returning 0.0.0.0 or specific sinkhole IPs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
)
from api_app.choices import Classification

from .data_model import MaliciousDetectorResponseDataModelMixin

logger = logging.getLogger(__name__)


class WebRisk(classes.ObservableAnalyzer):
class WebRisk(MaliciousDetectorResponseDataModelMixin, classes.ObservableAnalyzer):
"""Check if observable analyzed is marked as malicious by Google WebRisk API

Get these secrets from a Service Account valid file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from api_app.analyzers_manager.exceptions import AnalyzerRunException

from ..dns_responses import malicious_detector_response
from .data_model import MaliciousDetectorResponseDataModelMixin


class MockUpSafeBrowsing:
Expand All @@ -27,7 +28,7 @@ def lookup_urls(urls: List[str]) -> Dict:
}


class GoogleSF(classes.ObservableAnalyzer):
class GoogleSF(MaliciousDetectorResponseDataModelMixin, classes.ObservableAnalyzer):
"""Check if observable analyzed is marked as malicious for Google SafeBrowsing"""

_api_key_name: str
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
)
from api_app.analyzers_manager.observable_analyzers.dns.doh_mixin import DoHMixin

from .data_model import MaliciousDetectorResponseDataModelMixin

logger = logging.getLogger(__name__)


class MullvadDNSAnalyzer(DoHMixin, ObservableAnalyzer):
class MullvadDNSAnalyzer(MaliciousDetectorResponseDataModelMixin, DoHMixin, ObservableAnalyzer):
"""
MullvadDNSAnalyzer:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@

from ..dns_responses import malicious_detector_response
from ..quad9_base import Quad9Base
from .data_model import MaliciousDetectorResponseDataModelMixin

logger = logging.getLogger(__name__)


class Quad9MaliciousDetector(Quad9Base, classes.ObservableAnalyzer):
class Quad9MaliciousDetector(MaliciousDetectorResponseDataModelMixin, Quad9Base, classes.ObservableAnalyzer):
"""Check if a domain is malicious by Quad9 public resolver.
Quad9 does not answer in the case a malicious domain is queried.
However, we need to perform another check to understand if that domain was blocked
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
from api_app.choices import Classification

from ..dns_responses import malicious_detector_response
from .data_model import MaliciousDetectorResponseDataModelMixin

logger = logging.getLogger(__name__)


class SpamhausWQS(classes.ObservableAnalyzer):
class SpamhausWQS(MaliciousDetectorResponseDataModelMixin, classes.ObservableAnalyzer):
url: str = "https://apibl.spamhaus.net/lookup/v1"
_api_key: str = None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
from api_app.choices import Classification

from ..dns_responses import malicious_detector_response
from .data_model import MaliciousDetectorResponseDataModelMixin


class UltraDNSMaliciousDetector(classes.ObservableAnalyzer):
class UltraDNSMaliciousDetector(MaliciousDetectorResponseDataModelMixin, classes.ObservableAnalyzer):
"""Resolve a DNS query with UltraDNS servers,
if the response falls within the sinkhole range, the domain is malicious.
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ def run(self):
found = PhishingArmyDomain.objects.filter(domain=to_analyze_observable).exists()
return {"found": found, "link": self.url}

# Gate on a real listing hit: the $malicious mapping constant would otherwise
# stamp MALICIOUS on every clean lookup.
def _do_create_data_model(self) -> bool:
return super()._do_create_data_model() and self.report.report.get("found") is True

@classmethod
def update(cls) -> bool:
try:
Expand Down
5 changes: 5 additions & 0 deletions api_app/analyzers_manager/observable_analyzers/phishstats.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class PhishStats(ObservableAnalyzer):
def update(cls) -> bool:
pass

# Gate on a real listing hit: the $malicious mapping constant would otherwise
# stamp MALICIOUS on every clean lookup.
def _do_create_data_model(self) -> bool:
return super()._do_create_data_model() and bool(self.report.report.get("results"))

def __build_phishstats_url(self) -> str:
to_analyze_observable_classification = self.observable_classification
to_analyze_observable_name = self.observable_name
Expand Down
21 changes: 21 additions & 0 deletions api_app/analyzers_manager/observable_analyzers/phishtank.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@
from api_app.analyzers_manager.classes import ObservableAnalyzer
from api_app.analyzers_manager.exceptions import AnalyzerRunException
from api_app.choices import Classification
from api_app.data_model_manager.enums import DataModelEvaluations

logger = logging.getLogger(__name__)

# A community-verified phishing hit is a strong malicious signal; an unverified
# listing is deliberately given reliability 5 — below MALICIOUS_RELIABILITY_FLOOR
# (6) — so it buckets to "suspicious" rather than "malicious".
_RELIABILITY_VERIFIED = 8
_RELIABILITY_UNVERIFIED = 5


class Phishtank(ObservableAnalyzer):
_api_key_name: str
Expand Down Expand Up @@ -41,3 +48,17 @@ def run(self):
except requests.RequestException as e:
raise AnalyzerRunException(e)
return result

# Gate: the $-mapping would stamp MALICIOUS on any created model, so only a
# real listing (in_database) may create one.
def _do_create_data_model(self) -> bool:
results = self.report.report.get("results") or {}
return super()._do_create_data_model() and results.get("in_database") is True

def _update_data_model(self, data_model) -> None:
super()._update_data_model(data_model)
results = self.report.report.get("results") or {}
data_model.evaluation = DataModelEvaluations.MALICIOUS.value
data_model.reliability = (
_RELIABILITY_VERIFIED if results.get("verified") is True else _RELIABILITY_UNVERIFIED
)
Loading
Loading