Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions threat_intel/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CHANGELOG - Threat Intel

<!-- towncrier release notes start -->
56 changes: 56 additions & 0 deletions threat_intel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Threat Intel

## Overview

This integration queries the [AbuseIPDB](https://www.abuseipdb.com/) threat intelligence API to check IP addresses for known malicious activity and sends the results as logs to Datadog.

The check periodically queries the configured IP addresses against the AbuseIPDB database, reporting abuse confidence scores, ISP information, country of origin, and report counts.

## Setup

### Prerequisites

You need an AbuseIPDB API key. You can sign up for a free account at [AbuseIPDB](https://www.abuseipdb.com/).

### Installation

The Threat Intel check is included in the [Datadog Agent][1] package. No additional installation is needed on your server.

### Configuration

1. Edit the `threat_intel.d/conf.yaml` file in the `conf.d/` folder at the root of your Agent's configuration directory to start collecting threat intelligence data. See the [sample threat_intel.d/conf.yaml][2] for all available configuration options.

2. [Restart the Agent][3].

### Validation

[Run the Agent's status subcommand][4] and look for `threat_intel` under the Checks section.

## Data Collected

### Logs

The Threat Intel check sends log events containing threat intelligence data for each queried IP address, including:

- IP address
- Abuse confidence score
- Country code
- ISP
- Domain
- Total reports
- Whitelist status
- Last reported timestamp

### Service Checks

**threat_intel.can_connect**: Returns `CRITICAL` if the check fails to query the AbuseIPDB API. Returns `OK` otherwise.

## Support

Need help? Contact [Datadog support][5].

[1]: https://app.datadoghq.com/account/settings/agent/latest
[2]: https://github.com/DataDog/integrations-core/blob/master/threat_intel/datadog_checks/threat_intel/data/conf.yaml.example
[3]: https://docs.datadoghq.com/agent/guide/agent-commands/#start-stop-and-restart-the-agent
[4]: https://docs.datadoghq.com/agent/guide/agent-commands/#agent-status-and-information
[5]: https://docs.datadoghq.com/help/
46 changes: 46 additions & 0 deletions threat_intel/assets/configuration/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Threat Intel
fleet_configurable: true
files:
- name: threat_intel.yaml
options:
- template: logs
example:
- type: integration
service: threat_intel
source: threat_intel
- template: init_config
options:
- template: init_config/default
- template: instances
options:
- name: api_key
fleet_configurable: true
required: true
secret: true
description: "AbuseIPDB API key for querying IP threat intelligence."
value:
type: string
example: <ABUSEIPDB_API_KEY>
- name: ip_addresses
fleet_configurable: true
required: true
description: "List of IP addresses to check for threat intelligence."
value:
type: array
items:
type: string
example:
- 192.168.1.1
- name: max_age_in_days
fleet_configurable: true
description: "Maximum age in days for abuse reports."
value:
type: integer
example: 90
minimum: 1
maximum: 365

- template: instances/default
overrides:
min_collection_interval.value.example: 3600
min_collection_interval.value.minimum: 60
11 changes: 11 additions & 0 deletions threat_intel/assets/service_checks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
{
"agent_version": "7.0.0",
"integration": "threat_intel",
"check": "threat_intel.can_connect",
"statuses": ["ok", "critical"],
"groups": [],
"name": "threat_intel.can_connect",
"description": "Returns CRITICAL if the check cannot query the AbuseIPDB API. Returns OK otherwise."
}
]
1 change: 1 addition & 0 deletions threat_intel/changelog.d/22626.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Initial Release
4 changes: 4 additions & 0 deletions threat_intel/datadog_checks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# (C) Datadog, Inc. 2025-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
__path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
4 changes: 4 additions & 0 deletions threat_intel/datadog_checks/threat_intel/__about__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# (C) Datadog, Inc. 2025-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
__version__ = '0.0.1'
7 changes: 7 additions & 0 deletions threat_intel/datadog_checks/threat_intel/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# (C) Datadog, Inc. 2025-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
from .__about__ import __version__
from .check import ThreatIntelCheck

__all__ = ['__version__', 'ThreatIntelCheck']
79 changes: 79 additions & 0 deletions threat_intel/datadog_checks/threat_intel/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# (C) Datadog, Inc. 2025-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
import json

from datadog_checks.base import AgentCheck, ConfigurationError
from datadog_checks.base.utils.time import get_current_datetime, get_timestamp

from . import constants


class ThreatIntelCheck(AgentCheck):
__NAMESPACE__ = "threat_intel"

def __init__(self, name, init_config, instances):
super(ThreatIntelCheck, self).__init__(name, init_config, instances)
self.api_key = self.instance.get("api_key")
self.ip_addresses = self.instance.get("ip_addresses", [])
self.max_age_in_days = self.instance.get("max_age_in_days", 90)
self.check_initializations.append(self.validate_config)

def validate_config(self) -> None:
if not self.api_key:
raise ConfigurationError("AbuseIPDB API key is required.")
if not self.ip_addresses:
raise ConfigurationError("At least one IP address must be configured.")

def query_ip(self, ip_address: str) -> dict | None:
"""Query the AbuseIPDB API for threat intelligence on an IP address."""
url = constants.ABUSEIPDB_API_URL
headers = {"Key": self.api_key, "Accept": "application/json"}
params = {
"ipAddress": ip_address,
"maxAgeInDays": str(self.max_age_in_days),
}
try:
response = self.http.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
except Exception:
self.log.error("Failed to query AbuseIPDB for IP %s", ip_address)
raise

def check(self, _):
current_time = get_current_datetime()
has_error = False
for ip_address in self.ip_addresses:
try:
result = self.query_ip(ip_address)
if result and "data" in result:
data = result["data"]
log_data = {
"timestamp": get_timestamp(current_time),
"message": json.dumps(
{
"ip_address": data.get("ipAddress"),
"abuse_confidence_score": data.get("abuseConfidenceScore"),
"country_code": data.get("countryCode"),
"isp": data.get("isp"),
"domain": data.get("domain"),
"total_reports": data.get("totalReports"),
"is_whitelisted": data.get("isWhitelisted"),
"last_reported_at": data.get("lastReportedAt"),
}
),
"ddsource": "threat_intel",
}
self.send_log(log_data)
except Exception:
has_error = True

if has_error:
self.service_check(
constants.SERVICE_CHECK_NAME,
AgentCheck.CRITICAL,
message="Failed to query one or more IP addresses.",
)
else:
self.service_check(constants.SERVICE_CHECK_NAME, AgentCheck.OK)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# (C) Datadog, Inc. 2026-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)

# This file is autogenerated.
# To change this file you should edit assets/configuration/spec.yaml and then run the following commands:
# ddev -x validate config -s <INTEGRATION_NAME>
# ddev -x validate models -s <INTEGRATION_NAME>

from .instance import InstanceConfig
from .shared import SharedConfig


class ConfigMixin:
_config_model_instance: InstanceConfig
_config_model_shared: SharedConfig

@property
def config(self) -> InstanceConfig:
return self._config_model_instance

@property
def shared_config(self) -> SharedConfig:
return self._config_model_shared
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# (C) Datadog, Inc. 2026-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)

# This file is autogenerated.
# To change this file you should edit assets/configuration/spec.yaml and then run the following commands:
# ddev -x validate config -s <INTEGRATION_NAME>
# ddev -x validate models -s <INTEGRATION_NAME>


def instance_disable_generic_tags():
return False


def instance_empty_default_hostname():
return False


def instance_max_age_in_days():
return 90


def instance_min_collection_interval():
return 3600
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# (C) Datadog, Inc. 2026-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)

# This file is autogenerated.
# To change this file you should edit assets/configuration/spec.yaml and then run the following commands:
# ddev -x validate config -s <INTEGRATION_NAME>
# ddev -x validate models -s <INTEGRATION_NAME>

from __future__ import annotations

from typing import Optional

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

from datadog_checks.base.utils.functions import identity
from datadog_checks.base.utils.models import validation

from . import defaults, validators


class MetricPatterns(BaseModel):
model_config = ConfigDict(
arbitrary_types_allowed=True,
frozen=True,
)
exclude: Optional[tuple[str, ...]] = None
include: Optional[tuple[str, ...]] = None


class InstanceConfig(BaseModel):
model_config = ConfigDict(
validate_default=True,
arbitrary_types_allowed=True,
frozen=True,
)
api_key: str
disable_generic_tags: Optional[bool] = None
empty_default_hostname: Optional[bool] = None
ip_addresses: tuple[str, ...]
max_age_in_days: Optional[int] = Field(None, ge=1, le=365)
metric_patterns: Optional[MetricPatterns] = None
min_collection_interval: Optional[float] = Field(None, ge=60.0)
service: Optional[str] = None
tags: Optional[tuple[str, ...]] = None

@model_validator(mode='before')
def _initial_validation(cls, values):
return validation.core.initialize_config(getattr(validators, 'initialize_instance', identity)(values))

@field_validator('*', mode='before')
def _validate(cls, value, info):
field = cls.model_fields[info.field_name]
field_name = field.alias or info.field_name
if field_name in info.context['configured_fields']:
value = getattr(validators, f'instance_{info.field_name}', identity)(value, field=field)
else:
value = getattr(defaults, f'instance_{info.field_name}', lambda: value)()

return validation.utils.make_immutable(value)

@model_validator(mode='after')
def _final_validation(cls, model):
return validation.core.check_model(getattr(validators, 'check_instance', identity)(model))
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# (C) Datadog, Inc. 2026-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)

# This file is autogenerated.
# To change this file you should edit assets/configuration/spec.yaml and then run the following commands:
# ddev -x validate config -s <INTEGRATION_NAME>
# ddev -x validate models -s <INTEGRATION_NAME>

from __future__ import annotations

from typing import Optional

from pydantic import BaseModel, ConfigDict, field_validator, model_validator

from datadog_checks.base.utils.functions import identity
from datadog_checks.base.utils.models import validation

from . import validators


class SharedConfig(BaseModel):
model_config = ConfigDict(
validate_default=True,
arbitrary_types_allowed=True,
frozen=True,
)
service: Optional[str] = None

@model_validator(mode='before')
def _initial_validation(cls, values):
return validation.core.initialize_config(getattr(validators, 'initialize_shared', identity)(values))

@field_validator('*', mode='before')
def _validate(cls, value, info):
field = cls.model_fields[info.field_name]
field_name = field.alias or info.field_name
if field_name in info.context['configured_fields']:
value = getattr(validators, f'shared_{info.field_name}', identity)(value, field=field)

return validation.utils.make_immutable(value)

@model_validator(mode='after')
def _final_validation(cls, model):
return validation.core.check_model(getattr(validators, 'check_shared', identity)(model))
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# (C) Datadog, Inc. 2026-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)

# Here you can include additional config validators or transformers
#
# def initialize_instance(values, **kwargs):
# if 'my_option' not in values and 'my_legacy_option' in values:
# values['my_option'] = values['my_legacy_option']
# if values.get('my_number') > 10:
# raise ValueError('my_number max value is 10, got %s' % str(values.get('my_number')))
#
# return values
Loading
Loading