Skip to content

feat(api): add ASN-aggregated IOC statistics . CLOSES #458#718

Merged
regulartim merged 7 commits intointelowlproject:developfrom
drona-gyawali:feat/asn_api
Jan 27, 2026
Merged

feat(api): add ASN-aggregated IOC statistics . CLOSES #458#718
regulartim merged 7 commits intointelowlproject:developfrom
drona-gyawali:feat/asn_api

Conversation

@drona-gyawali
Copy link
Copy Markdown
Contributor

@drona-gyawali drona-gyawali commented Jan 17, 2026

Description

This change introduces a new authenticated API endpoint that aggregates IOC data by ASN. The endpoint groups all matching IOCs under their respective ASNs and computes summary statistics, including IOC count, total attack count, total interaction count, total login attempts, expected IOC count (derived from recurrence probability), and expected interactions. It also returns the set of unique honeypots associated with each ASN. The implementation reuses the same filtering and authentication logic as the Advanced Feeds API to avoid code duplication, while intentionally returning a JSON-only response tailored for aggregated data use cases.

  • Reuses existing Advanced Feeds query building logic to avoid duplication
  • Aggregation logic is isolated in a utility function
  • Floating point values are rounded to 4 decimals for stable output
  • Includes test coverage following existing feeds test patterns

Related issues

closes #458

Type of change

Please delete options that are not relevant.

  • 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.
  • Linters (Black, Flake, Isort) 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.

Important Rules

  • If you miss to compile the Checklist properly, your PR won't be reviewed by the maintainers.
  • If your changes decrease the overall tests coverage (you will know after the Codecov CI job is done), you should add the required tests to fix the problem
  • Everytime you make changes to the PR and you think the work is done, you should explicitly ask for a review. After being reviewed and received a "change request", you should explicitly ask for a review again once you have made the requested changes.

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!

A few general things:

  1. In my mind the aggregation would be done on a database level. Your approach also works, of course, but I am a little bit concerned regarding performance on instances with millions of IoCs.
  2. Ordering is kind of tricky here. I tested your code and I was not able to order by ioc_count. But intuitively I would expect that to work. I think we should support to order by any field that is present in the output, if sensible (ordering by honeypots is not).
  3. The IOC model has more field that have not been included here yet. Do you think you could also include first_seen and last_seen? Or do you think that this is not useful?

@drona-gyawali
Copy link
Copy Markdown
Contributor Author

drona-gyawali commented Jan 18, 2026

Nice, thank you!

A few general things:

1. In my mind the aggregation would be done on a database level. Your approach also works, of course, but I am a little bit concerned regarding performance on instances with millions of IoCs.

2. Ordering is kind of tricky here. I tested your code and I was not able to order by `ioc_count`. But intuitively I would expect that to work. I think we should support to order by any field that is present in the output, if sensible (ordering by `honeypots` is not).

3. The IOC model has more field that have not been included here yet. Do you think you could also include `first_seen` and `last_seen`? Or do you think that this is not useful?

Hi, @regulartim, Thanks a lot for the review and the feedback!

I wanted to explain a bit why I initially went with Python-level aggregation and where I’m currently unsure, so I’d really appreciate your guidance.

My first thought was also to aggregate at the database level using values("asn") + annotate(...). However, I ran into a couple of structural issues with the current setup of get_queryset, which is shared by other API:

get_queryset applies slicing ([:feed_size]) after ordering. Once a queryset is sliced, Django does not allow further aggregation, which makes DB-level grouping impossible unless we disable slicing using a flag (doSlice=True). However i was afraid to change the fx which is used by other apis without your permission.

Error i recieved using this method:
Screenshot from 2026-01-18 18-23-21

And If we slice before aggregation, the aggregation sees only the first feed_size raw IOCs. This produces incorrect totals (ioc_count, attack_count, etc.), because other IOCs outside the slice are ignored. For example, if ASN 13335 has 10,000 IOCs, slicing at 5000 will undercount.

When aggregating by ASN and joining against general_honeypot, each IOC can appear multiple times at the sql level (one row per honeypot). This leads to inflated sums (e.g., attack_count, interaction_count).

For example, a single IOC linked to two honeypots is counted twice in SUM(attack_count) in aggregation . I guees it is many to many joins and duplicated rows issue.

I think that get_queryset needs a big refactor here

I’d love to hear what you think is the best tradeoff here, especially given the shared nature of get_queryset. I’m happy to adapt the implementation based on your recommendation.

@regulartim
Copy link
Copy Markdown
Collaborator

Hey @drona-gyawali ! Thanks for your detailed explanation. I think the best approach would be to aggregate on the DB level. If this makes it necessary to refactor or even split up get_queryset, that's fine. I recognize that it is too inflexible with the slicing and the way it queries the DB. If that's not possible, it is also possible to write a separate function, exclusively for this API.

When aggregating by ASN and joining against general_honeypot, each IOC can appear multiple times at the sql level (one row per honeypot). This leads to inflated sums (e.g., attack_count, interaction_count).

This is kind of strange. If a aggregation function is used on the general_honeypot filed, this should not happen. Did you use ArrayAgg?

@drona-gyawali
Copy link
Copy Markdown
Contributor Author

Hey @drona-gyawali ! Thanks for your detailed explanation. I think the best approach would be to aggregate on the DB level. If this makes it necessary to refactor or even split up get_queryset, that's fine. I recognize that it is too inflexible with the slicing and the way it queries the DB. If that's not possible, it is also possible to write a separate function, exclusively for this API.

When aggregating by ASN and joining against general_honeypot, each IOC can appear multiple times at the sql level (one row per honeypot). This leads to inflated sums (e.g., attack_count, interaction_count).

This is kind of strange. If a aggregation function is used on the general_honeypot filed, this should not happen. Did you use ArrayAgg?

Extremely sorry for the late reply! I used ArrayAgg(distinct=True) for honeypots and distinct=True on all other fields to avoid inflated sums, since thing were failing without it. The iocs_qs comes from get_queryset. raw code for reference:

def asn_aggregated_queryset(iocs_qs):   
    return (
        iocs_qs
        .exclude(asn__isnull=True)
        .values("asn")
        .annotate(
            ioc_count=Count("id", distinct=True),
            total_attack_count=Sum("attack_count", distinct=True),
            total_interaction_count=Sum("interaction_count", distinct=True),
            total_login_attempts=Sum("login_attempts", distinct=True),
            expected_ioc_count=Sum("recurrence_probability", distinct=True),
            expected_interactions=Sum("expected_interactions", distinct=True),
            honeypots=ArrayAgg("general_honeypot__name", distinct=True),
        )
    )

I’ll push the new version soon.

@regulartim
Copy link
Copy Markdown
Collaborator

Extremely sorry for the late reply!

Don't worry, we all have other stuff to do! :)

I’ll push the new version soon.

Cool, looking forward to that!

@drona-gyawali drona-gyawali marked this pull request as draft January 22, 2026 16:37
@drona-gyawali
Copy link
Copy Markdown
Contributor Author

Hi @regulartim , In this new version, I implemented DB-level aggregation for the ASN feed. While building this, I had to introduce a few things:

New serializer (ASNFeedsOrderingSerializer) – I inherited from FeedsRequestSerializer because the base serializer already provides default handling for parameters like max_age, feed_type, and attack_type. The main reason for creating the new serializer was that the base serializer’s ordering validation is strict and only allows model fields. Since aggregation introduces annotated/non-model fields (like ioc_count, total_attack_count), I needed to add custom validation here.

resolve_aggregation_ordering utility – This ensures that our aggregation endpoint defaults to ordering by -ioc_count instead of -last_seen, bypassing the default injection from feed_params. I tried to make this dynamic so that any future aggregation API can leverage the same pattern without reinventing the wheel.

The overall goal was to make the developer experience better and avoid complexity when building future aggregation endpoints. Anyone adding a new aggregation API only needs to inherit and customize the ordering/validation, without rewriting everything.

I hope this aligns with your expectations. If you feel any part of this design needs changes, I’m always open to feedback and happy to adjust.

@drona-gyawali drona-gyawali marked this pull request as ready for review January 22, 2026 17:02
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.

Good progress! 👍

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.

Good work! 👍 This basically looks good to merge, I only found two minor things.

Since this is a new API endpoint, it would be great to include it into our documentation. Would you please do that? If so, do you need guidance for that?

@drona-gyawali
Copy link
Copy Markdown
Contributor Author

drona-gyawali commented Jan 27, 2026

Good work! 👍 This basically looks good to merge, I only found two minor things.

Since this is a new API endpoint, it would be great to include it into our documentation. Would you please do that? If so, do you need guidance for that?

Raised a pr in our docs repo: intelowlproject/docs#46

@regulartim regulartim merged commit 82713d4 into intelowlproject:develop Jan 27, 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