On this page

Getting custom data into Microsoft Sentinel has traditionally required a lot of moving parts. You need a Data Collection Endpoint, a Data Collection Rule, an Entra app registration with a client secret, RBAC role assignments, a custom table definition, and usually an Azure Function to glue it all together. That’s six manual steps before you even write your first KQL query.

Microsoft’s Codeless Connector Framework (CCF) Push mode, now in public preview, collapses all of that into a single deploy action. You define your connector in JSON, click deploy in the Sentinel Data Connectors gallery, and Sentinel auto-provisions the DCE, DCR, custom table, Entra app registration, client secret, and Monitoring Metrics Publisher RBAC assignment. You get back connection credentials and a push endpoint โ€” ready to receive data.

This matters now because the legacy Data Collector API (MMA-based) retires on September 14, 2026. If you’re still using POST https://<workspace-id>.ods.opinsights.azure.com/api/logs, start migrating.

Hands-on Lab: All connector artifacts, deployment scripts, analytics rules, and the Python sender are in the companion lab. Deploy-Lab.ps1 deploys infra, analytics rules, and the workbook. The CCF Push connector resources (DCE/DCR/table/app/secret) are provisioned by clicking Deploy Push Connector Resources in the Sentinel portal.


What is CCF Push?

The Codeless Connector Framework has two modes:

  • Poll mode โ€” Sentinel pulls data from an API on a schedule (good for SaaS APIs with rate limits)
  • Push mode โ€” Your application pushes data to a DCE endpoint via OAuth (good for real-time feeds, custom collectors, and migration from the legacy API)

Push mode is the focus of this post because it solves the hardest integration pattern: getting arbitrary external data into Sentinel without building Azure Functions or Logic Apps.

What Gets Auto-Provisioned

When you click “Deploy Push Connector Resources” in the Sentinel data connectors gallery, CCF Push creates:

ResourceWhat it does
Data Collection Endpoint (DCE)HTTPS endpoint that accepts your JSON payloads
Data Collection Rule (DCR)Transforms and routes data to the custom table
Custom Log Analytics tableFeodoTracker_CL with your defined schema
Entra ID app registrationService principal for OAuth authentication
Client secretCredential for the app registration (shown once)
RBAC role assignmentMonitoring Metrics Publisher on the DCR

Old Way vs CCF Push

StepManual SetupCCF Push
Create DCEaz monitor data-collection endpoint createAuto
Define custom tableaz monitor log-analytics workspace table createAuto
Create DCR with transformsaz monitor data-collection rule createAuto
Register Entra app + secretAzure Portal โ†’ App RegistrationsAuto
Assign RBACaz role assignment createAuto
Build sender applicationAzure Function / Logic AppYou write this
Total manual steps61 (+ sender script)

The sender application is the only thing you build yourself. Everything else is handled by the framework.

Microsoft Sentinel Data Connectors page showing the Feodotracker Botnet C2 Feed (CCF Push) connector at the top of the list, with the detail panel displaying description, last data received timestamp, and a data ingestion chart showing 5 records
The Feodotracker CCF Push connector in the Sentinel Data Connectors gallery. The status shows "Disconnected" because the push resources (DCE/DCR/app) haven't been provisioned yet via the portal deploy button โ€” but data is already flowing from our manual setup, as the ingestion chart confirms.

The Data Source: abuse.ch Feodotracker

Feodotracker is a free threat intelligence feed maintained by abuse.ch that tracks botnet command-and-control (C2) server infrastructure. It covers major malware families including Dridex, Emotet, TrickBot, QakBot, BumbleBee, Pikabot, and others.

The feed provides:

  • IP addresses of confirmed C2 servers
  • Port numbers used for C2 communication
  • Malware family attribution
  • First seen / last seen timestamps
  • Status (online, offline)
  • Country of the hosting infrastructure

The blocklist JSON endpoint requires no authentication:

https://feodotracker.abuse.ch/downloads/ipblocklist.json

This is real, continuously updated threat intelligence โ€” not synthetic test data. Feeding it into Sentinel gives you an immediately actionable threat intelligence table that can correlate against your network logs.


Lab Deployment

Prerequisites

  • Azure subscription (free trial works)
  • PowerShell 7.0+ with Azure CLI installed and authenticated
  • Python 3.10+ with pip
  • Roles: Contributor + Microsoft Sentinel Contributor on the target resource group

Deploy

./scripts/Deploy-Lab.ps1 -Location "eastus"

Note: The script deploys infrastructure, analytics rules, and the workbook. After it completes, open the Sentinel Data Connectors gallery and click Deploy Push Connector Resources on the Feodotracker connector to auto-provision the DCE, DCR, custom table, and Entra app.

What Gets Deployed

ResourceTypePurpose
Log Analytics workspaceMicrosoft.OperationalInsights/workspacesData storage
Sentinel onboardingMicrosoft.SecurityInsights/onboardingStatesEnable Sentinel
CCF Push connectorData connector (Push kind)Auto-provisions DCE/DCR/table/app
FeodoTracker_CLCustom tableThreat intelligence storage
5 analytics rulesScheduled KQLThreat detection + TI correlation
1 workbookSentinel workbookThreat intel dashboard (5 panels)

Cost Estimate

  • Log Analytics ingestion: ~$2.76/GB (pay-as-you-go)
  • Feodotracker feed: ~500 indicators per batch โ‰ˆ negligible ingestion cost
  • No compute costs (no Azure Functions)
  • Total: < $3/month for lab workloads

Building the CCF Push Connector

The connector consists of four JSON artifacts that define the table schema, data collection rule, connector UI, and push configuration.

Step 1: Define the Custom Table Schema

The table schema maps to the Feodotracker JSON fields. Every custom table in Log Analytics requires a TimeGenerated column of type datetime.

{
  "properties": {
    "schema": {
      "name": "FeodoTracker_CL",
      "columns": [
        { "name": "TimeGenerated", "type": "datetime", "description": "Ingestion timestamp" },
        { "name": "ip_address", "type": "string", "description": "C2 server IP address" },
        { "name": "port", "type": "int", "description": "C2 communication port" },
        { "name": "status", "type": "string", "description": "C2 server status (online/offline)" },
        { "name": "malware", "type": "string", "description": "Malware family name" },
        { "name": "first_seen", "type": "datetime", "description": "When the C2 was first observed" },
        { "name": "last_seen", "type": "datetime", "description": "When the C2 was last observed" },
        { "name": "country", "type": "string", "description": "Hosting country code" }
      ]
    }
  }
}

Step 2: Create the Data Collection Rule

The DCR defines the input stream schema and a transform KQL query. For this connector, we use a pass-through transform that adds TimeGenerated = now() for records that arrive without a timestamp.

{
  "properties": {
    "dataCollectionEndpointId": "[auto-provisioned]",
    "streamDeclarations": {
      "Custom-FeodoTrackerStream": {
        "columns": [
          { "name": "ip_address", "type": "string" },
          { "name": "port", "type": "int" },
          { "name": "status", "type": "string" },
          { "name": "malware", "type": "string" },
          { "name": "first_seen", "type": "datetime" },
          { "name": "last_seen", "type": "datetime" },
          { "name": "country", "type": "string" }
        ]
      }
    },
    "dataFlows": [
      {
        "streams": ["Custom-FeodoTrackerStream"],
        "destinations": ["logAnalyticsWorkspace"],
        "transformKql": "source | extend TimeGenerated = now()",
        "outputStream": "Custom-FeodoTracker_CL"
      }
    ]
  }
}

The transformKql field is where you can enrich, filter, or reshape data before it lands in the table. For this lab, source | extend TimeGenerated = now() is all we need.

Step 3: Create the Connector Definition

The connector definition controls how the connector appears in the Sentinel Data Connectors gallery โ€” the icon, description, instructions, and the deploy button.

{
  "kind": "Customizable",
  "properties": {
    "connectorUiConfig": {
      "title": "Feodotracker Botnet C2 Feed (CCF Push)",
      "publisher": "Nine Lives, Zero Trust (Lab)",
      "descriptionMarkdown": "Ingests botnet C2 indicators from abuse.ch Feodotracker...",
      "graphQueriesTableName": "FeodoTracker_CL",
      "dataTypes": [
        {
          "name": "FeodoTracker_CL",
          "lastDataReceivedQuery": "FeodoTracker_CL | summarize max(TimeGenerated)"
        }
      ],
      "connectivityCriteria": [
        {
          "type": "HasDataConnectors"
        }
      ],
      "permissions": {
        "resourceProvider": [
          {
            "provider": "Microsoft.OperationalInsights/workspaces",
            "permissionsDisplayText": "Read and Write permissions on the workspace",
            "requiredPermissions": { "write": true, "read": true, "delete": true }
          }
        ]
      },
      "instructionSteps": [
        {
          "title": "Deploy Push Connector Resources",
          "description": "Click the button below to auto-provision the DCE, DCR, custom table, and Entra app registration.",
          "instructions": [
            {
              "type": "DeployPushConnectorButton"
            }
          ]
        }
      ]
    }
  }
}

The "type": "DeployPushConnectorButton" instruction is what creates the deploy button. When clicked, Sentinel provisions all the resources listed in the architecture section.

Step 4: Create the Push Data Connector

This ties the connector definition to the push configuration:

{
  "kind": "Push",
  "properties": {
    "connectorDefinitionName": "FeodotrackerCCFPush",
    "dcrConfig": {
      "streamName": "Custom-FeodoTrackerStream",
      "dataCollectionEndpoint": "[auto]",
      "dataCollectionRuleId": "[auto]"
    }
  }
}

Step 5: Deploy and Collect Credentials

After deploying the connector artifacts via the REST API (or Deploy-Lab.ps1), open the Sentinel Data Connectors gallery, find the “Feodotracker Botnet C2 Feed” connector, and click Deploy Push Connector Resources.

Sentinel displays the connection credentials:

  • Tenant ID โ€” your Entra tenant
  • Client ID โ€” the auto-provisioned app registration
  • Client Secret โ€” shown once, copy it immediately
  • DCE URI โ€” the Data Collection Endpoint URL
  • DCR Immutable ID โ€” identifies the Data Collection Rule
  • Stream Name โ€” Custom-FeodoTrackerStream

Save these โ€” you’ll need them for the sender script.


The Sender Application

The Python script fetches C2 indicators from abuse.ch, transforms them to match the table schema, authenticates via OAuth 2.0 client credentials, and POSTs batches to the DCE.

#!/usr/bin/env python3
"""Fetch abuse.ch Feodotracker C2 indicators and push to Sentinel via CCF Push."""

import json
import os
import sys
import requests
from datetime import datetime, timezone

FEODO_URL = "https://feodotracker.abuse.ch/downloads/ipblocklist.json"
BATCH_SIZE = 100

def get_oauth_token(tenant_id: str, client_id: str, client_secret: str) -> str:
    url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
    resp = requests.post(url, data={
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret,
        "scope": "https://monitor.azure.com//.default",
    })
    resp.raise_for_status()
    return resp.json()["access_token"]

def fetch_indicators() -> list[dict]:
    resp = requests.get(FEODO_URL, timeout=30)
    resp.raise_for_status()
    return resp.json()

def transform(indicators: list[dict]) -> list[dict]:
    records = []
    for ind in indicators:
        records.append({
            "ip_address": ind.get("ip_address", ""),
            "port": ind.get("port", 0),
            "status": ind.get("status", ""),
            "malware": ind.get("malware", ""),
            "first_seen": ind.get("first_seen", ""),
            "last_seen": ind.get("last_online", ""),
            "country": ind.get("country", ""),
        })
    return records

def send_batch(records, dce_uri, dcr_id, stream_name, token):
    url = (f"{dce_uri}/dataCollectionRules/{dcr_id}"
           f"/streams/{stream_name}?api-version=2023-01-01")
    resp = requests.post(url, json=records, headers={
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    })
    resp.raise_for_status()
    return resp.status_code

The full script with batching logic, error handling, and environment variable support is in scripts/Send-ThreatIntel.py.

Key implementation details:

  • OAuth scope: https://monitor.azure.com//.default (note the double slash โ€” this is required)
  • Batch size: 100 records per POST to stay within the 1MB payload limit
  • POST endpoint: {dce_uri}/dataCollectionRules/{dcr_id}/streams/{stream_name}?api-version=2023-01-01
  • Ingestion delay: First batch takes 5-10 minutes to appear in the table; subsequent batches are faster
  • Scheduling: Run via cron, Azure Automation, or GitHub Actions for continuous ingestion

Sentinel Analytics Rules

Microsoft Defender portal Analytics page showing 5 Active rules with severity bar (3 High, 2 Medium), the Active rules tab selected, and the rules grid with LAB rules visible
Five analytics rules deployed in the Defender portal โ€” 3 High severity (New Botnet Family, High-Confidence Active C2, Network Traffic to Known C2) and 2 Medium (C2 Infrastructure Surge, Geographic Concentration). Scroll down in the portal to see all five.

Five scheduled analytics rules detect patterns in the Feodotracker data. The first four analyze the threat intelligence feed itself. The fifth โ€” the most valuable โ€” correlates C2 indicators against your actual network traffic.

Rule 1: New Botnet Family Detected

Fires when a malware family appears in the feed for the first time โ€” no historical records in the last 30 days.

let KnownFamilies = FeodoTracker_CL
    | where TimeGenerated > ago(30d) and TimeGenerated < ago(1h)
    | summarize arg_max(TimeGenerated, *) by ip_address
    | distinct malware;
FeodoTracker_CL
| where TimeGenerated > ago(1h)
| summarize arg_max(TimeGenerated, *) by ip_address
| where malware !in (KnownFamilies)
| summarize IndicatorCount = dcount(ip_address),
    FirstIP = min(ip_address),
    Countries = make_set(country, 10)
    by malware
| project TimeGenerated = now(), malware,
    IndicatorCount, FirstIP, Countries

Why this matters: A new malware family appearing in the C2 feed indicates a new campaign or a previously unknown botnet infrastructure becoming active. This is an early warning signal.

Rule 2: C2 Infrastructure Surge

Detects a >50% increase in active C2 IPs compared to the previous 24-hour window.

let Current = FeodoTracker_CL
    | where TimeGenerated > ago(1h)
    | where status == "online"
    | summarize CurrentCount = dcount(ip_address)
    | extend _key = 1;
let Previous = FeodoTracker_CL
    | where TimeGenerated between (ago(2d) .. ago(1d))
    | where status == "online"
    | summarize PreviousCount = dcount(ip_address)
    | extend _key = 1;
Current | join kind=inner (Previous) on _key
| where PreviousCount > 0
| extend ChangePercent = round(100.0 * (CurrentCount - PreviousCount) / PreviousCount, 1)
| where ChangePercent > 50
| project TimeGenerated = now(), CurrentCount,
    PreviousCount, ChangePercent

Why this matters: A sudden spike in active C2 infrastructure often precedes a large-scale spam or malware campaign. Operators spin up servers before launching.

Rule 3: High-Confidence Active C2

Flags recently active C2 servers using encrypted communication ports (443, 8443) โ€” the most likely to evade network-level detection.

FeodoTracker_CL
| where TimeGenerated > ago(1h)
| summarize arg_max(TimeGenerated, *) by ip_address
| where status == "online"
| where port in (443, 8443)
| where last_seen > ago(7d)
| project TimeGenerated, ip_address, port,
    malware, country, first_seen, last_seen

Why this matters: C2 traffic over port 443 blends with legitimate HTTPS traffic. These indicators are the highest priority for network blocking rules and firewall policies.

Rule 4: Geographic C2 Concentration

Alerts when 10+ C2 IPs from the same country appear in a single ingestion batch, indicating concentrated infrastructure.

FeodoTracker_CL
| where TimeGenerated > ago(1h)
| summarize C2Count = dcount(ip_address),
    Families = make_set(malware, 10),
    Ports = make_set(port, 10),
    SampleIPs = make_set(ip_address, 5)
    by country
| where C2Count >= 10
| project TimeGenerated = now(), country, C2Count,
    Families, Ports, SampleIPs

Why this matters: C2 concentration in a single country can indicate a bulletproof hosting provider or a compromised hosting infrastructure. It’s also useful for building geographic blocklists.

Rule 5: Network Traffic to Known Botnet C2

This is the rule that turns your passive threat intelligence into active detection. It joins the Feodotracker C2 IP list against your actual network traffic logs โ€” CommonSecurityLog (firewalls, proxies), DnsEvents (DNS resolutions), or any other log source with destination IPs.

let ActiveC2 = FeodoTracker_CL
    | where TimeGenerated > ago(7d)
    | where status == "online"
    | distinct ip_address, malware, port;
union isfuzzy=true
    (datatable(TimeGenerated:datetime, SourceIP:string,
        DestinationIP:string, LogSource:string,
        Details:string)[]),
    (CommonSecurityLog
        | where TimeGenerated > ago(1d)
        | where isnotempty(DestinationIP)
        | project TimeGenerated, SourceIP, DestinationIP,
            LogSource = DeviceProduct, Details = Activity),
    (DnsEvents
        | where TimeGenerated > ago(1d)
        | where isnotempty(IPAddresses)
        | mv-expand IPAddress = split(IPAddresses, ",")
        | project TimeGenerated, SourceIP = ClientIP,
            DestinationIP = tostring(IPAddress),
            LogSource = "DNS", Details = Name)
| join kind=inner ActiveC2
    on $left.DestinationIP == $right.ip_address
| project TimeGenerated, SourceIP, DestinationIP,
    malware, LogSource, Details

Why this matters: The previous four rules tell you what’s happening in the threat landscape. This rule tells you whether any of it is happening in your environment. A match here means a device in your network is actively communicating with a confirmed botnet C2 server.

The union isfuzzy=true with an empty datatable fallback ensures the rule deploys and runs even if you don’t have CommonSecurityLog or DnsEvents tables yet โ€” it gracefully handles missing tables instead of failing.

Extending the correlation: Add more log sources to the union to widen coverage:

  • AzureNetworkAnalytics_CL for NSG flow logs
  • AZFWNetworkRule for Azure Firewall
  • DeviceNetworkEvents for Defender for Endpoint
  • Syslog with parsed destination IPs for Linux hosts

MITRE ATT&CK Mapping

TechniqueIDDetection
Application Layer ProtocolT1071Rules 1, 3, 5
Encrypted ChannelT1573Rule 3
Acquire InfrastructureT1583Rules 2, 4
Web ServiceT1102Rule 5

Hunting Queries

Five proactive hunting queries for threat intelligence analysis. Run these manually during investigations or scheduled hunts.

Hunt 1: C2 Infrastructure by Malware Family Over Time

FeodoTracker_CL
| where TimeGenerated > ago(30d)
| summarize C2Servers = dcount(ip_address)
    by malware, bin(TimeGenerated, 1d)
| render timechart

Track how each botnet’s infrastructure grows or shrinks over time. Useful for understanding campaign tempo.

Hunt 2: Most Active C2 Countries (Last 30 Days)

FeodoTracker_CL
| where TimeGenerated > ago(30d)
| where status == "online"
| summarize ActiveC2 = dcount(ip_address),
    Families = make_set(malware, 20)
    by country
| sort by ActiveC2 desc
| take 20

Identify which countries host the most active C2 infrastructure. Cross-reference with your organization’s geographic exposure.

Hunt 3: Newly Appeared C2 IPs (First Seen in Last 7 Days)

FeodoTracker_CL
| where TimeGenerated > ago(7d)
| where first_seen > ago(7d)
| summarize arg_max(TimeGenerated, *) by ip_address
| project ip_address, port, malware, country,
    first_seen, last_seen, status
| sort by first_seen desc

Fresh C2 infrastructure is the most dangerous โ€” it hasn’t made it into most blocklists yet.

Hunt 4: Long-Lived C2 Infrastructure (Active > 90 Days)

FeodoTracker_CL
| where TimeGenerated > ago(1d)
| where status == "online"
| extend DaysActive = datetime_diff('day', now(), first_seen)
| where DaysActive > 90
| summarize arg_max(TimeGenerated, *) by ip_address
| project ip_address, port, malware, country,
    first_seen, DaysActive
| sort by DaysActive desc

C2 servers that survive 90+ days are either in bulletproof hosting or have been missed by takedown efforts. These are high-value blocklist candidates.

Hunt 5: Feed Ingestion Health Check

FeodoTracker_CL
| summarize
    RecordCount = count(),
    DistinctIPs = dcount(ip_address),
    Families = dcount(malware),
    Countries = dcount(country),
    OnlineCount = countif(status == "online"),
    OldestRecord = min(first_seen),
    NewestRecord = max(last_seen)
    by bin(TimeGenerated, 6h)
| extend OnlinePercent = round(
    100.0 * OnlineCount / RecordCount, 1)
| sort by TimeGenerated desc

Audit the freshness and completeness of your feed. Gaps in the 6-hour bins mean missed ingestion runs โ€” check your cron job, GitHub Actions, or client secret expiry.


Workbook: Threat Intelligence Dashboard

The workbook provides five panels for ongoing threat intelligence monitoring.

Panel 1: C2 Activity Timeline

Timechart showing indicator count by malware family over time. Spot campaigns ramping up or winding down.

FeodoTracker_CL
| where TimeGenerated {TimeRange}
| summarize Indicators = dcount(ip_address) by malware, bin(TimeGenerated, 1d)
| render timechart

Panel 2: Geographic Distribution

Bar chart of C2 server count by country. Identify hosting hotspots.

FeodoTracker_CL
| where TimeGenerated {TimeRange}
| where status == "online"
| summarize C2Servers = dcount(ip_address) by country
| sort by C2Servers desc
| take 15
| render barchart

Panel 3: Active Malware Families

Table of malware families with active C2 count, latest activity, and top countries.

FeodoTracker_CL
| where TimeGenerated {TimeRange}
| summarize ActiveC2 = dcount(ip_address),
    LatestActivity = max(last_seen),
    TopCountries = make_set(country, 5)
    by malware
| sort by ActiveC2 desc

Panel 4: Recent Indicators

Table of the latest C2 indicators with full metadata, sorted by ingestion time.

FeodoTracker_CL
| where TimeGenerated {TimeRange}
| sort by TimeGenerated desc
| project TimeGenerated, ip_address, port, malware,
    status, country, first_seen, last_seen
| take 50

Panel 5: Network Traffic to Known C2

Table showing cross-source matches between your network traffic and active C2 indicators. This is the panel SOC analysts will use most โ€” it answers “is any of this threat intelligence relevant to my environment?”

let ActiveC2 = FeodoTracker_CL
| where TimeGenerated {TimeRange}
| where status == "online"
| distinct ip_address, malware;
union isfuzzy=true
    (datatable(TimeGenerated:datetime, SourceIP:string,
        DestinationIP:string, LogSource:string)[]),
    (CommonSecurityLog
        | where TimeGenerated {TimeRange}
        | where isnotempty(DestinationIP)
        | project TimeGenerated, SourceIP, DestinationIP,
            LogSource = DeviceProduct),
    (DnsEvents
        | where TimeGenerated {TimeRange}
        | where isnotempty(IPAddresses)
        | mv-expand IPAddress = split(IPAddresses, ",")
        | project TimeGenerated, SourceIP = ClientIP,
            DestinationIP = tostring(IPAddress),
            LogSource = "DNS")
| join kind=inner ActiveC2
    on $left.DestinationIP == $right.ip_address
| project TimeGenerated, SourceIP, DestinationIP,
    malware, LogSource
| sort by TimeGenerated desc
| take 50

Automated Scheduling with GitHub Actions

The companion repo includes a GitHub Actions workflow that runs Send-ThreatIntel.py every 6 hours. Clone the repo, add your connection credentials as repository secrets, and you have a continuously updating threat intelligence pipeline โ€” no Azure Functions, no compute costs, just GitHub’s free tier.

name: Ingest Feodotracker C2 Indicators

on:
  schedule:
    - cron: '0 */6 * * *'
  workflow_dispatch:

jobs:
  ingest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install requests
      - name: Push indicators to Sentinel
        env:
          CCF_TENANT_ID: ${{ secrets.CCF_TENANT_ID }}
          CCF_CLIENT_ID: ${{ secrets.CCF_CLIENT_ID }}
          CCF_CLIENT_SECRET: ${{ secrets.CCF_CLIENT_SECRET }}
          CCF_DCE_URI: ${{ secrets.CCF_DCE_URI }}
          CCF_DCR_ID: ${{ secrets.CCF_DCR_ID }}
        run: python3 scripts/Send-ThreatIntel.py

To set up:

  1. Fork or clone j-dahl7/sentinel-ccf-push-connector
  2. Go to Settings โ†’ Secrets and variables โ†’ Actions
  3. Add the 5 connection credentials from the CCF Push deploy step
  4. Enable the workflow โ€” indicators start flowing every 6 hours

The workflow_dispatch trigger lets you run it manually for testing. GitHub Actions free tier includes 2,000 minutes/month โ€” this workflow uses about 1 minute per run, so 4 runs/day ร— 30 days = 120 minutes. Well within limits.


Extending to Other Feeds

The same CCF Push pattern works for any data source that produces JSON. abuse.ch maintains several other free feeds that map directly to the same architecture:

FeedURLWhat It TracksSchema
Feodotracker (this lab)feodotracker.abuse.chBotnet C2 server IPsIP, port, malware, country
URLhausurlhaus.abuse.chMalware distribution URLsURL, threat type, host, tags
ThreatFoxthreatfox.abuse.chIOCs (IPs, domains, hashes)IOC type, value, threat type, malware
MalwareBazaarbazaar.abuse.chMalware samplesSHA256, filename, signature, tags

For each feed, you would:

  1. Define a new custom table schema (e.g., URLhaus_CL)
  2. Create a new DCR with the appropriate stream and transform
  3. Add a new connector definition to the Sentinel gallery
  4. Write a sender script (or extend Send-ThreatIntel.py with a --feed parameter)

The CCF Push connector definition and DCR templates in this lab can be adapted by changing the table name, column definitions, and transform KQL. The authentication and push mechanics are identical.


Old Way vs New Way

If you’ve built custom Sentinel connectors before, this comparison captures the shift:

AspectLegacy (DCE/DCR Manual)CCF Push
Resource provisioning6 separate az commands1 click in Sentinel gallery
Entra app managementManual registration + secret rotationAuto-provisioned, secret shown on deploy
RBAC configurationManual role assignmentAuto-assigned Monitoring Metrics Publisher
Compute costsAzure Function consumption (~$5-15/month)None (you run the sender anywhere)
Connector UI in SentinelNone (hidden plumbing)Full gallery entry with status, last data received
MaintenanceFunction runtime updates, secret rotationZero (just run your sender script)
ARM template supportYes (complex, 3+ resources)Yes (single connector resource)
Migration effort from legacy APIHigh (rebuild everything)Low (change the POST endpoint + auth)

The biggest win isn’t the automation โ€” it’s the visibility. Your custom connector shows up in the Sentinel Data Connectors gallery alongside Microsoft’s first-party connectors, with connection status, last data received timestamp, and a proper configuration UI.


Microsoft Defender portal Incidents page showing 2 High-severity incidents โ€” LAB - High-Confidence Active C2 (priority 28) and LAB - New Botnet Family Detected (priority 16) โ€” with alerts from Microsoft Sentinel scheduled detections
Two High-severity incidents in the Defender portal โ€” the QakBot C2 server on port 443 and the first appearance of Emotet and QakBot families in the feed. Both triggered automatically from Feodotracker data.

Key Takeaways

  1. CCF Push eliminates the biggest friction point in getting custom data into Sentinel. No more manual DCE/DCR/app registration choreography.

  2. The legacy Data Collector API retires September 14, 2026. If you’re using the old https://<workspace-id>.ods.opinsights.azure.com/api/logs endpoint, plan your migration now. CCF Push is the replacement path.

  3. Push-based beats poll-based for real-time feeds. You control when data arrives. No polling intervals, no Lambda/Function compute costs, no cold-start delays.

  4. Correlate TI with your network traffic. A threat intel feed is informational until you join it against your logs. Rule 5 turns passive indicators into active detections by matching C2 IPs against CommonSecurityLog, DnsEvents, and any other network log source.

  5. abuse.ch feeds are free, reliable, and immediately actionable. Feodotracker is one of many feeds (URLhaus, MalwareBazaar, ThreatFox) that can be ingested with the same CCF Push pattern.

  6. Automate with GitHub Actions for zero-cost scheduling. The companion repo includes a workflow that ingests every 6 hours on GitHub’s free tier. No Azure Functions, no Logic Apps, no compute costs.

  7. CCF Push supports ARM templates for the connector definition, DCR, and table schema โ€” all declarative JSON suitable for CI/CD pipelines. The DCE/DCR/app provisioning still requires the portal deploy step, but the rest of the stack is fully automatable.


Resources

Jerrad Dahlager

Jerrad Dahlager, CISSP, CCSP

Cloud Security Architect ยท Adjunct Instructor

Marine Corps veteran and firm believer that the best security survives contact with reality.

Have thoughts on this post? I'd love to hear from you.