Skip to content

feat: Add Tor exit node extraction with separate TorExitNode model (#…#728

Merged
regulartim merged 3 commits intointelowlproject:developfrom
Sumit-ai-dev:add-tor-exit-nodes
Jan 26, 2026
Merged

feat: Add Tor exit node extraction with separate TorExitNode model (#…#728
regulartim merged 3 commits intointelowlproject:developfrom
Sumit-ai-dev:add-tor-exit-nodes

Conversation

@Sumit-ai-dev
Copy link
Copy Markdown
Contributor

@Sumit-ai-dev Sumit-ai-dev commented Jan 22, 2026

Description

This PR adds automatic extraction and filtering of Tor exit node IPs to reduce false positives in threat feeds. Tor exit nodes are privacy servers for anonymous browsing, not attackers - but they show up in honeypot logs and get incorrectly flagged as malicious.

Following the same approach as IntelOwl, this fetches the official Tor exit node list from torproject.org and filters them out, just like we do with mass scanners.

What I Built

Created TorExitNode model

  • Fields: ip_address (unique), added, reason
  • Separate model for semantic clarity

Added cron job (greedybear/cronjobs/tor_exit_nodes.py)

  • Downloads the Tor exit node list from https://check.torproject.org/exit-addresses
  • Validates IP addresses using existing is_valid_ipv4 utility
  • Stores valid IPs in the database
  • Updates ip_reputation field for existing IOCs

Built repository layer (greedybear/cronjobs/repositories/tor.py)

  • get_or_create() method for data access
  • Follows same pattern as MassScannerRepository and FireHolRepository

Hooked into Celery (get_tor_exit_nodes task in tasks.py)

  • Scheduled execution via Celery Beat

Wrote comprehensive tests (tests/test_tor.py)

  • Repository operations (creation, retrieval, no duplicates)
  • Cron job execution (fetch, validation, error handling)
  • IOC reputation updates

Notes

Currently handles IPv4 addresses only (what Tor Project publishes). Uses regex-based extraction for consistency with IntelOwl.

Files Changed

New:

  • greedybear/cronjobs/repositories/tor.py
  • greedybear/cronjobs/tor_exit_nodes.py
  • tests/test_tor.py

Modified:

  • greedybear/models.py - Added TorExitNode model
  • greedybear/tasks.py - Added get_tor_exit_nodes task
  • greedybear/cronjobs/repositories/__init__.py - Export TorRepository

Related issues

Closes #547

Type of change

  • Bug fix (non-breaking change which fixes an issue).
  • New feature (non-breaking change which adds functionality).
  • Breaking change (fix or feature that would cause existing functionality to not work as expected).

Checklist

  • I have read and understood the rules about how to Contribute to this project.
  • The pull request is for the branch develop.
  • I have added documentation of the new features.
  • Linter (Ruff) gave 0 errors. If you have correctly installed pre-commit, it does these checks and adjustments on your behalf.
  • I have added tests for the feature/bug I solved. All the tests (new and old ones) gave 0 errors.
  • If changes were made to an existing model/serializer/view, the docs were updated and regenerated (check CONTRIBUTE.md).
  • If the GUI has been modified:
    • I have a provided a screenshot of the result in the PR.
    • I have created new frontend tests for the new component or updated existing ones.

Copy link
Copy Markdown
Collaborator

@regulartim regulartim left a comment

Choose a reason for hiding this comment

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

Nice!

Comment on lines +81 to +85
@shared_task()
def get_tor_exit_nodes():
from greedybear.cronjobs.tor_exit_nodes import TorExitNodesCron

TorExitNodesCron().execute()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you forgot to schedule this task in celery.py.

from greedybear.cronjobs.tor_exit_nodes import TorExitNodesCron


class TestTorRepository(unittest.TestCase):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please use CustomTestCase as a base class for consistency with other tests.

self.assertFalse(created)


class TestTorExitNodesCron(unittest.TestCase):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here

Thanks for the review! Made both changes:
- Switched to CustomTestCase for test consistency
- Added weekly Celery Beat schedule (Sundays at 4:30 AM)

Addresses feedback from @regulartim
@Sumit-ai-dev
Copy link
Copy Markdown
Contributor Author

Hey @regulartim!

Thanks for catching those! Both fixed now:

  • Switched test classes to use CustomTestCase instead of unittest.TestCase
  • Added the Celery Beat schedule in celery.py

CI is all green ✅. Let me know if you spot anything else!

Copy link
Copy Markdown
Collaborator

@regulartim regulartim left a comment

Choose a reason for hiding this comment

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

Looks good so far, aside from the lower-case bug. However, there are still things missing for this to work:

  • a migration file
  • a registration of the new model in admin.py


def _update_old_ioc(self, ip_address: str):
"""Update the IP reputation of an existing IOC to mark it as a Tor exit node."""
updated = self.ioc_repo.update_ioc_reputation(ip_address, "Tor Exit Node")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here, "Tor Exit Node" has to be lower case, otherwise the filtering at the API-level won't work, right?

- Added migration file for TorExitNode model (0032)
- Fixed case sensitivity issue - changed 'Tor Exit Node' to lowercase for API filtering
- Registered TorExitNode in admin panel for easy management
@Sumit-ai-dev
Copy link
Copy Markdown
Contributor Author

Hey! Thanks for the review - really appreciate the feedback.

I've gone through and addressed all three points:

Migration file - Added 0032_torexitnode.py with proper dependencies.

Case sensitivity - Changed all the "Tor Exit Node" strings to lowercase "tor exit node" across the codebase (models, repository, cron job, tests, migration). This should fix the API filtering issue you mentioned.

Admin registration - TorExitNode is now registered in the admin panel with search by IP address.

All the checks are passing now! Let me know if anything needs tweaking or if you have other suggestions.

Copy link
Copy Markdown
Collaborator

@regulartim regulartim left a comment

Choose a reason for hiding this comment

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

Nice, thank you!

@regulartim regulartim merged commit 4b5ec02 into intelowlproject:develop Jan 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants