Page 1 of 1

Historical IP address analysis for Intrusion Prevention

Posted: Sat Nov 25, 2017 5:07 pm
by tomfisk
In my post titled Suricata IDS/IPS integration with Mikrotik (now with OSSEC) I provided a system to:
  • scan network traffic going through a Mikrotik router,
  • creating IPS (Intrusion Prevention System) events that would,
  • trigger the creation of firewall rules to prevent access to the target network.
These IPS events are triggered by Suricata IDS (Intrusion Detection System) [http://suricata-ids.org] and OSSEC HIDS (Host Intrusion Detection System) [https://ossec.github.io/]. IP addresses that are blocked by this system are stored in a MySQL table (block_queue). In my implementation, I chose not to purge this table as I anticipated being able to do analysis on the data I collected over time (the topic of this post).

In my implementation, only specific events flagged by Suricata or OSSEC are blocked. For these IPS events, the system blocks a specific IP address for a predetermined length of time. Depending on the threat severity that each individual IPS event posed to the network, that IP address would be blocked for a specific length of time from the network. IPS events trigger a firewall rule that expire anywhere from 1 hour to 2 days.

In the 14 months this system has been active on my home laboratory network, it has collected approximately 400,000 IDS and HIDS events, representing approximately 100,000 distinct IP addresses. Simple statistics say that 4 events originated from each of the 100,000 IP addresses. But this is definitely not the case. There are specific IP address ranges that represent a high quantity of intrusion events. I call these IP address ranges “frequent offenders”. As I looked at the volume of data, I wanted to be able to identity these address ranges (Classless Inter-Doman Routing, CIDR) so that I could create permanent firewall rules to block these address ranges. Doing so would, hopefully, cut down on a significant portion of the events from entering the network in the first place. My goal was to block as much traffic as possible (33-50%) from these “frequent offenders” using the minimum number of address ranges.

Bad actors who want to penetrate networks have focused strategies to minimize their intrusion profile. Coordinating attacks over multiple, non-adjacent network addresses and over time, make it difficult for Security Information and Event Management (SIEM) systems to correlate intrusion events as they occur. My assumption is that these bad actors have access to a finite pool of addresses, that over a longer period of time, will appear as clusters of IP addresses with a higher concentration of events. Permanently blocking these address clusters should reduce (hopefully significantly) the number of attempts by bad actors to gain access to resources on my network.

Information about my network
I’ll give you some basic information about my network so that you can understand my data and methodologies that I used for looking for clusters of addresses in my IPS events. My home laboratory network is located in the Jakarta, Indonesia metropolitan area. From the events I’ve seen coming into my home laboratory network, it is apparent that there is minimal or no IDS/IPS occurring at the edge of my Internet Service Provider’s network. I also maintain a network in the US, and the type and number of events coming into that network, flagged by the same IDS and HIDS, are significantly fewer. Given this, in the analysis that follows, you’ll see that addresses from Asia Pacific region are a significant portion of my IDS/HIDS events.

I maintain a permanent IP Tunnel between my Indonesia and US networks as well as a VPN connection between my home Indonesia network and my employer’s network. I also maintain, at various times, VPN connections to other networks in the US. For purposes of this analysis, I would have to ensure that I did not create a firewall rule that would prohibit these inter-network connections.

Finding clusters of offenders
In order to find the frequent offenders that we should block, we need to be able to find significant clusters of offenders that have triggered events on the network. In this article, I provide two different ways of clustering offenders. The first is clustering the offenders by ASN (Autonomous System Number) address ranges, and the second is by clustering using Class C networks.

Both of these methods will provide you with the ability to permanently block a significant portion of the IP addresses that constitute the frequent offenders. Of course, the need for you implement these blocks on these address ranges is not only a decision on the threat that they represent to your network, but is also a business decision based on the whether or not your organization wants to block addresses where a threat did not originate from. Let’s say there is one bad actor, that has persistently created events against your network, in an address range of x.x.x.x/16. You’d be blocking the other 65533 addresses from this ASN. In other words, you might be throwing out the baby with the bath water. This might not be an issue if your network is used primarily for intra-organization purposes. But if you are running an e-commerce website, you’ll have to determine if you want to potentially block customers from this ASN. If you’re not selling to customers in China or Russia, for example, then there should be no harm.

Clustering offenders by ASN (Autonomous System Number) address range
Wikipedia defines an ASN as “an ISP must have an officially registered autonomous system number (ASN). A unique ASN is allocated to each AS for use in BGP routing. AS numbers are important because the ASN uniquely identifies each network on the Internet.”

This analysis method uses BGP (Border Gateway Protocol) data in order to see what clusters emerge from the threats over time. This analysis method is not without some problems, however. BGP data changes over time, address ranges are consolidated, split, and reassigned. Applying a current set of BGP data to a historical set of data will result in some anomalies. In this analysis, I hope that end result of these anomalies will be minimal. Unfortunately, there is no way to know exactly what the impact of these anomalies are as I would have to process historical BGP data matching my historical data. If BGP data changes slowly over time (my assumption) then the anomalies are minimized. I believe that using current BGP data against the historical data collected by the IDS, still gives valuable results.

Finding a “complete” set of BGP data mapped to ASN’s is not necessarily an easy process. There are API services that use queries to each of the Regional Internet Registry (RIR) databases to get the ASN for the specific address, and the ASN’s address range. These API’s are often limited to a specific (and sometimes very small) number of queries per day. In the end, I decided to use the BGP and ASN data available as a file from the Asia Pacific NIC (http://thyme.apnic.net/current). This dataset proved to be the most complete and up-to-date that I could find available on the internet.

Here is a flow description that describes the general method for processing the data:
  1. If this is the first run:
    1. Download the ASN and BGP data, loading it into local MySQL tables
    2. For addresses we want to exclude, find their BGP/ASN data and mark as excluded (will prevent firewall rules from being created)
  2. While there are unprocessed “block_queue” records,
    1. Get unprocessed entries from the “block_queue” table (up 10,000 rows)
    2. Check if an address range record already exists for this address
      1. Yes, add 1 to the offender count
      2. No,
        1. find the smallest address range for this address from the BGP and ASN data
        2. Was an address range found?
          1. Yes, add the address range record using BGP and ASN data
          2. No, add a single address record
The attached PHP program “offenders_by_asn.php” implements this process flow.

In order to process the IDS/HIDS events I’ve collected over the 14 months, I set up a cron job that runs the script “process_offenders.sh” every 15 minutes, and I limit the number of records processed to 10,000 in the program. This ensures that the background processing of the events won’t overwhelm my system.

After the processing is done for the current set of “block_queue” entries, the cron job can be removed (or commented out) or you can leave the process to run on a regular basis. If you want to re-run the entire analysis against all “block_queue” records, simply TRUNCATE the “offenders_process” table (TRUNCATE TABLE offenders_process), and start the process over again.

Results from clustering by ASN
After processing approximately 400,000 events, 28,750 ASN records were created. 383 of these records were created as single address records because a corresponding BGP address range was not found. Creating a firewall rule for the top 100 offending ASN address ranges would block about 160,000 events, or 40%. This definitely falls within my goal range. Table 1 shows the top 100 offenders.
Table1.pdf
Clustering offenders using Class C networks
Microsoft Technet defines a Class C network as “Class C addresses are used for small networks. The three high-order bits in a class C address are always set to binary 1 1 0. The next 21 bits (completing the first three octets) complete the network ID. The remaining 8 bits (last octet) represent the host ID. This allows for 2,097,152 networks and 254 hosts per network.”

This analysis method is fairly straight-forward. First, collect all of the unique IP addresses that have flagged an event. Next, iterate over all of the Class C network spaces (x.x.x.x/24) and check the number of addresses in that space that have triggered an event. If there are more addresses in this space than our trigger threshold (I set the trigger to 16 hosts), then mark the address space as a frequent offender. This analysis method is not concerned with the total number of events coming from an address range, just the number of hosts that have triggered an event from the Class C space.

As this analysis method is not as database intensive as the analysis by ASN, the analysis program “offenders_by_clc.php” is run interactively. On my system, it took about 3 hours to run across all of the 14,461,947 Class C address spaces.

Results from clustering by Class C spaces
After processing approximately 400,000 events, representing about 100,000 unique IP addresses, with a cluster threshold of 16, 160 Class C records were created. Creating a firewall rule for these 160 Class C address ranges would block about 61,200 events, or 15%. While this is not within my goal range, it still represents a modest amount of traffic that would be blocked.

Comparison of the Analysis Methods
The ASN clustering method blocks more traffic (40%) but blocks a significantly larger number of hosts. Creating firewall rules for the address ranges from the Top 100 ASN frequent offenders blocks about 12.7 million IPv4 addresses, or about 0.35% of the public addresses. The Class C clustering method with a threshold of 16 hosts per Class C space takes a much finer approach as it blocks only 15% of the traffic. But creating firewall rules for the address ranges from all of the Class C frequent offenders blocks a meager 40,640 hosts, or about 0.000011% of the public addresses.

Applying firewall rules from Cluster Analysis
The php script “top_offender_block.php” creates address list entries (list name “Perm Blocked”) in order to block the top offenders address ranges. If both analyses have been performed, it will ask which analysis method to use to create the address list. For the Class C analysis, all address ranges in the table will be used to create the address list. For the ASN analysis, it prompts for the number of records to use (default 100) . Any existing entries in the list “Perm Blocked” will be deleted before the addresses are added.

Summary
Depending on how your network is utilized, home/business/e-commerce/business-to-business, etc., and your tolerance for bad actors having any access to your network, the analyses presented here may or may not be of interest to you. If you feel that Suricata/OSSEC and other security layers/practices are protecting your network sufficiently, then this may not be of much value for you. However, if your security stance is to block as many bad actors as possible before they get a chance to probe your network, then see what the results of these analyses can provide for you.

Finally, if you have an idea for an additional analysis, please share your ideas here.

Implementing the Analyses

Here's the good stuff!

Mikrotik Setup
Set up firewall rules to block inbound and outbound traffic based on an address list:
add action=drop chain=input comment="Permanently blocked bad actors" src-address-list="Perm Blocked"
add action=drop chain=forward comment="Drop any traffic going to permanently blocked bad actors" dst-address-list="Perm Blocked"
I have the input chain rule following the rule for addresses in the "Blocked". Similar with the forward chain rule.

MySQL - New Tables
Add the following tables to the snorby database in MySQL:
-- phpMyAdmin SQL Dump
-- version 4.7.5
-- https://www.phpmyadmin.net/
--
-- Server version: 5.7.17
-- PHP Version: 7.0.22-3

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";

--
-- Database: `snorby`
--

-- --------------------------------------------------------

--
-- Table structure for table `asn_bgp`
--

CREATE TABLE `asn_bgp` (
  `bgp_id` int(11) NOT NULL,
  `bgp_cidr` varchar(20) NOT NULL,
  `bgp_start_address` varchar(20) NOT NULL,
  `bgp_end_address` varchar(20) NOT NULL,
  `bgp_start_range` int(11) UNSIGNED NOT NULL,
  `bgp_end_range` int(11) UNSIGNED NOT NULL,
  `asn_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `asn_bgp`
--
ALTER TABLE `asn_bgp`
  ADD PRIMARY KEY (`bgp_id`),
  ADD KEY `bgp_start_range` (`bgp_start_range`),
  ADD KEY `bgp_end_range` (`bgp_end_range`),
  ADD KEY `asn_id` (`asn_id`),
  ADD KEY `bgp_start_end` (`bgp_start_range`,`bgp_end_range`);

--
-- AUTO_INCREMENT for dumped tables
--

--
-- AUTO_INCREMENT for table `asn_bgp`
--
ALTER TABLE `asn_bgp`
  MODIFY `bgp_id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";

--
-- Database: `snorby`
--

-- --------------------------------------------------------

--
-- Table structure for table `asn_name`
--

CREATE TABLE `asn_name` (
  `asn_id` int(11) NOT NULL,
  `asn_description` varchar(128) NOT NULL,
  `asn_country_code` varchar(4) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `asn_name`
--
ALTER TABLE `asn_name`
  ADD PRIMARY KEY (`asn_id`);
COMMIT;

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";

--
-- Database: `snorby`
--

-- --------------------------------------------------------

--
-- Table structure for table `offenders_by_asn`
--

CREATE TABLE `offenders_by_asn` (
  `asn_id` int(11) NOT NULL,
  `asn_bgp_prefix` varchar(20) NOT NULL,
  `asn_start_address` varchar(16) NOT NULL,
  `asn_end_address` varchar(16) NOT NULL,
  `asn_start_range` int(11) UNSIGNED NOT NULL,
  `asn_end_range` int(11) UNSIGNED NOT NULL,
  `asn_number` int(11) NOT NULL,
  `asn_registry` varchar(16) DEFAULT NULL,
  `asn_country` varchar(12) DEFAULT NULL,
  `asn_description` varchar(248) DEFAULT NULL,
  `asn_offender_count` int(11) NOT NULL DEFAULT '0',
  `asn_is_blocked` tinyint(1) NOT NULL DEFAULT '0',
  `asn_is_excluded` tinyint(1) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `offenders_by_asn`
--
ALTER TABLE `offenders_by_asn`
  ADD PRIMARY KEY (`asn_id`),
  ADD KEY `asn_start_range` (`asn_start_range`),
  ADD KEY `asn_end_range` (`asn_end_range`),
  ADD KEY `asn_number` (`asn_number`),
  ADD KEY `asn_is_blocked` (`asn_is_blocked`),
  ADD KEY `asn_start_end` (`asn_start_range`,`asn_end_range`);

--
-- AUTO_INCREMENT for dumped tables
--

--
-- AUTO_INCREMENT for table `offenders_by_asn`
--
ALTER TABLE `offenders_by_asn`
  MODIFY `asn_id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";

--
-- Database: `snorby`
--

-- --------------------------------------------------------

--
-- Table structure for table `offenders_by_classc`
--

CREATE TABLE `offenders_by_classc` (
  `clc_id` int(11) NOT NULL,
  `clc_bgp_prefix` varchar(20) NOT NULL,
  `clc_start_address` varchar(16) NOT NULL,
  `clc_end_address` varchar(16) NOT NULL,
  `clc_start_range` int(11) UNSIGNED NOT NULL,
  `clc_end_range` int(11) UNSIGNED NOT NULL,
  `clc_offender_count` int(11) NOT NULL DEFAULT '0',
  `clc_is_blocked` tinyint(1) NOT NULL DEFAULT '0',
  `clc_is_excluded` tinyint(1) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `offenders_by_classc`
--
ALTER TABLE `offenders_by_classc`
  ADD PRIMARY KEY (`clc_id`),
  ADD KEY `clc_start_range` (`clc_start_range`),
  ADD KEY `clc_end_range` (`clc_end_range`),
  ADD KEY `clc_start_end` (`clc_start_range`,`clc_end_range`);

--
-- AUTO_INCREMENT for dumped tables
--

--
-- AUTO_INCREMENT for table `offenders_by_classc`
--
ALTER TABLE `offenders_by_classc`
  MODIFY `clc_id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";

--
-- Database: `snorby`
--

-- --------------------------------------------------------

--
-- Table structure for table `offenders_process`
--

CREATE TABLE `offenders_process` (
  `proc_id` int(11) NOT NULL,
  `proc_last_queue_id` int(11) NOT NULL,
  `proc_start_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `proc_end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `proc_records_done` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `offenders_process`
--
ALTER TABLE `offenders_process`
  ADD PRIMARY KEY (`proc_id`);

--
-- AUTO_INCREMENT for dumped tables
--

--
-- AUTO_INCREMENT for table `offenders_process`
--
ALTER TABLE `offenders_process`
  MODIFY `proc_id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";

--
-- Database: `snorby`
--

-- --------------------------------------------------------

--
-- Table structure for table `offender_address`
--

CREATE TABLE `offender_address` (
  `oa_id` int(11) NOT NULL,
  `oa_ip_adr` varchar(16) NOT NULL,
  `oa_ip_numeric` int(11) UNSIGNED NOT NULL,
  `oa_offender_count` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Indexes for dumped tables
--

--
-- Indexes for table `offender_address`
--
ALTER TABLE `offender_address`
  ADD PRIMARY KEY (`oa_id`),
  ADD KEY `oa_ip_numeric` (`oa_ip_numeric`);

--
-- AUTO_INCREMENT for dumped tables
--

--
-- AUTO_INCREMENT for table `offender_address`
--
ALTER TABLE `offender_address`
  MODIFY `oa_id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;
PHP and bash scripts

offenders_by_asn.php - Place this in /usr/local/bin
<?php

/* Set your specific configuration below */
$user_name = "xxxxx";
$password = "password";
$database = "snorby";
$server = "localhost";
$max_records = 10000;
/* Indicate the IP addresses that should be excluded from the firewall rules, these will be mapped to their respective ASN BGP Prefix */
$excluded_addrs = array('1.2.3.4','5.6.7.8','9.10.11.12','13.14.15.16','192.168.100.2');
$records_processed = 0;
$start_time = "";
$last_queue_id = 0;

header('Content-Type: text/plain');

function show_status($done, $total, $size=30) {

    static $start_time;

    // if we go over our bound, just ignore it
    if($done > $total) return;
    if(empty($start_time)) $start_time=time();
    $now = time();
    $perc=(double)($done/$total);
    $bar=floor($perc*$size);
    $status_bar="\r[";
    $status_bar.=str_repeat("=", $bar);
    if($bar<$size){
        $status_bar.=">";
        $status_bar.=str_repeat(" ", $size-$bar);
    } else {
        $status_bar.="=";
    }
    $disp=number_format($perc*100, 0);
    $status_bar.="] $disp%  $done/$total";
    $rate = ($now-$start_time)/$done;
    $left = $total - $done;
    $eta = round($rate * $left, 2);
    $elapsed = $now - $start_time;
    $status_bar.= " remaining: ".number_format($eta)." sec.  elapsed: ".number_format($elapsed)." sec.";
    echo "$status_bar  ";
    flush();

    // when done, send a newline
    if($done == $total) {
        echo "\n";
    }

}

function v4CIDRtoMask($cidr) {
    $cidr = explode('/', $cidr);
    return array($cidr[0], long2ip(-1 << (32 - (int)$cidr[1])));
}

function ipv4Breakout ($ip_address, $ip_nmask) {
    //convert ip addresses to long form
    $ip_address_long = ip2long($ip_address);
    $ip_nmask_long = ip2long($ip_nmask);

    //caculate network address
    $ip_net = $ip_address_long & $ip_nmask_long;

    //caculate first usable address
    $ip_host_first = ((~$ip_nmask_long) & $ip_address_long);
    $ip_first = ($ip_address_long ^ $ip_host_first) + 1;

    //caculate last usable address
    $ip_broadcast_invert = ~$ip_nmask_long;
    $ip_last = ($ip_address_long | $ip_broadcast_invert) - 1;

    //caculate broadcast address
    $ip_broadcast = $ip_address_long | $ip_broadcast_invert;

    $block_info = array(array("network" => "$ip_net"),
            array("first_host" => "$ip_first"),
            array("last_host" => "$ip_last"),
            array("broadcast" => "$ip_broadcast"));

    return $block_info;
}

function downloadASNdata($thisdb) {
  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, 'http://thyme.apnic.net/current/data-used-autnums');
  curl_setopt($ch, CURLOPT_HEADER, 0);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
  $externalContent = curl_exec($ch);
  curl_close($ch);
  if (strlen($externalContent) == 0) {
    die('Unable to get data on ASN organizations from http://thyme.apnic.net/current/data-used-autnums');
  }
  $separator = "\r\n";
  $line = strtok($externalContent, $separator);

  while ($line !== false) {
    $row = preg_split('/ +/', trim($line), 2);
    $i = strrpos($row[1], ",");
    if ($i) {
      $countryCode = substr($row[1], $i + 2);
      if (strlen($countryCode) != 2) {
        $countryCode = '';
      }
    } else {
      $countryCode = '';
    }
    $SQL = "INSERT INTO asn_name (asn_id, asn_description, asn_country_code) VALUES ($row[0], '" . addslashes($row[1]) . "', '" . $countryCode . "');";
    if (!$result = $thisdb->query($SQL)) {
      die('There was an error running the query [' . $thisdb->error . '] ' . $SQL);
    }
    $line = strtok($separator);
  }

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, 'http://thyme.apnic.net/current/data-raw-table');
  curl_setopt($ch, CURLOPT_HEADER, 0);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
  $externalContent = curl_exec($ch);
  curl_close($ch);
  if (strlen($externalContent) == 0) {
    die('Unable to get raw data from http://thyme.apnic.net/current/data-raw-table');
  }
  $separator = "\r\n";
  $line = strtok($externalContent, $separator);

  while ($line !== false) {
    $row = explode("\t", trim($line));
    if (trim($row[1]) == '4259905537') { $row[1] = '42599'; } 
    $ipResult = v4CIDRtoMask(trim($row[0]));
    $blockInfo = ipv4Breakout($ipResult[0], $ipResult[1]);
    $SQL = "INSERT INTO asn_bgp (bgp_cidr, bgp_start_address, bgp_end_address, bgp_start_range, bgp_end_range, asn_id) VALUES ('" . $row[0] . "', '" .
      long2ip($blockInfo[1]["first_host"]) . "', '" . long2ip($blockInfo[2]["last_host"]) . 
      "', INET_ATON('" . long2ip($blockInfo[1]["first_host"]) . "'), INET_ATON('" . long2ip($blockInfo[2]["last_host"]) . "'), " . trim($row[1]) . ");";
    if (!$result = $thisdb->query($SQL)) {
      die('There was an error running the query [' . $thisdb->error . '] ' . $SQL);
    }
    $line = strtok($separator);
  }

}

/* Connect to database, if unsuccessful keep trying for 100 seconds */
    $i = 0;
    while ( $i < 100 ) {
      $db = new mysqli($server, $user_name, $password, $database);
      if ($db->connect_errno > 0) {
        print('Unable to connect to database [' . $db->connect_error . ']');
        sleep(10);
        $i = $i + 10;
      }
      else {
        $i = 100;
      }
    }

/* Main program loop */
/* Check if this is the first time we've run */
    $SQL = "SELECT * from offenders_process where proc_id > 0;";
    if (!$result = $db->query($SQL)) {
      die('There was an error running the query [' . $db->error . '] ' . $SQL);
    }
    /* No records in offenders_process, first time to run */
    if ($result->num_rows == 0) {

      $SQL = "TRUNCATE TABLE asn_name;";
      if (!$result = $db->query($SQL)) {
        die('There was an error running the query [' . $db->error . '] ' . $SQL);
      }
      $SQL = "TRUNCATE TABLE asn_bgp;";
      if (!$result = $db->query($SQL)) {
        die('There was an error running the query [' . $db->error . '] ' . $SQL);
      }
      $SQL = "TRUNCATE TABLE offenders_by_asn;";
      if (!$result = $db->query($SQL)) {
        die('There was an error running the query [' . $db->error . '] ' . $SQL);
      }
      downloadASNdata($db);

     /* Get ASN's for excluded addresses and mark as excluded */ 
      foreach ($excluded_addrs as $thisIP) {
        $SQL = "SELECT asn_bgp.*, asn_name.* from asn_bgp FORCE INDEX (bgp_start_end), asn_name where INET_ATON('" . $thisIP .
          "') BETWEEN bgp_start_range AND bgp_end_range" .
          " AND asn_bgp.asn_id = asn_name.asn_id ORDER BY SUBSTRING_INDEX(bgp_cidr, '/', 2) DESC LIMIT 1;"; 
        if (!$result = $db->query($SQL)) {
          die('There was an error running the query [' . $db->error . '] ' . $SQL);
        }
        if ($result->num_rows > 0) {
          /* Mark the entry for my ISP as excluded */
          $row = $result->fetch_assoc();
          $SQL = "INSERT INTO offenders_by_asn (asn_bgp_prefix, asn_start_address, asn_end_address, asn_start_range, asn_end_range, " .
            "asn_number, asn_country, asn_description, asn_is_excluded) VALUES('" . $row['bgp_cidr'] . "', '" . $row['bgp_start_address'] . 
            "', '" . $row['bgp_end_address'] . "', " . $row['bgp_start_range'] . ", " . $row['bgp_end_range'] . ", " . $row['asn_id'] . 
            ", '" . $row['asn_country_code'] . "', '" . addslashes($row['asn_description']) . "', 1);";
          if (!$result = $db->query($SQL)) {
            die('There was an error running the query [' . $db->error . '] ' . $SQL);
          }
        }
      }
      $SQL = "INSERT INTO offenders_process (proc_last_queue_id, proc_records_done) VALUES (0, 0);";
      if (!$result = $db->query($SQL)) {
        die('There was an error running the query [' . $db->error . '] ' . $SQL);
      }
    }

/* Start processing from the last point we left off */
    $SQL = "SELECT NOW() as rightnow;";
    if (!$result = $db->query($SQL)) {
      die('There was an error running the query [' . $db->error . '] ' . $SQL);
    }
    $row = $result->fetch_assoc();
    $start_time = $row['rightnow'];
    mysqli_free_result($result);
    $SQL = "SELECT * FROM offenders_process WHERE proc_id in (SELECT MAX(proc_id) FROM offenders_process);";
    if (!$result = $db->query($SQL)) {
      die('There was an error running the query [' . $db->error . '] ' . $SQL);
    }
    $row = $result->fetch_assoc();
    $SQL = "SELECT * FROM block_queue where que_id > " . $row['proc_last_queue_id'] . " LIMIT " . $max_records;
    if (!$result = $db->query($SQL)) {
      die('There was an error running the query [' . $db->error . '] ' . $SQL);
    }
    if ($result->num_rows > 0) {
      while($row = $result->fetch_assoc()) {
        if (strlen($row['que_ip_adr']) > 6) {
          $SQL2 = "SELECT * from offenders_by_asn where INET_ATON('" . $row['que_ip_adr'] . "') BETWEEN asn_start_range AND asn_end_range " .
            "ORDER BY SUBSTRING_INDEX(asn_bgp_prefix, '/', 2) DESC LIMIT 1;";
          if (!$result2 = $db->query($SQL2)) {
            die('There was an error running the query [' . $db->error . '] ' . $SQL2);
          }
          if ($result2->num_rows > 0) {
            /* Record for this ISP exists, update count */
            $row2 = $result2->fetch_assoc();
            $SQL3 = "UPDATE offenders_by_asn SET asn_offender_count = asn_offender_count + 1 where asn_id = " . $row2['asn_id'] . ";";
            if (!$result3 = $db->query($SQL3)) {
              die('There was an error running the query [' . $thisdb->error . '] ' . $SQL3);
            }
            mysqli_free_result($result2);
          } else {
            $SQL = "SELECT asn_bgp.*, asn_name.* from asn_bgp FORCE INDEX (bgp_start_end), asn_name where INET_ATON('" . $row['que_ip_adr'] . 
              "') BETWEEN bgp_start_range AND bgp_end_range" .
              " AND asn_bgp.asn_id = asn_name.asn_id ORDER BY SUBSTRING_INDEX(bgp_cidr, '/', 2) DESC LIMIT 1;";
            if (!$result2 = $db->query($SQL)) {
              die('There was an error running the query [' . $db->error . '] ' . $SQL);
            }
            if ($result2->num_rows > 0) {
              $row3 = $result2->fetch_assoc();
              $SQL = "INSERT INTO offenders_by_asn (asn_bgp_prefix, asn_start_address, asn_end_address, asn_start_range, asn_end_range, " .
                "asn_number, asn_country, asn_description, asn_offender_count) VALUES('" . $row3['bgp_cidr'] . "', '" . $row3['bgp_start_address'] .
                "', '" . $row3['bgp_end_address'] . "', " . $row3['bgp_start_range'] . ", " . $row3['bgp_end_range'] . ", " . $row3['asn_id'] .
                ", '" . $row3['asn_country_code'] . "', '" . addslashes($row3['asn_description']) . "', 1);";
              if (!$result2 = $db->query($SQL)) {
                die('There was an error running the query [' . $db->error . '] ' . $SQL);
              }
            } else {
              /* No ASN range defined for this event - log as a single address excpetion */
              $SQL = "INSERT INTO offenders_by_asn (asn_bgp_prefix, asn_start_address, asn_end_address, asn_start_range, asn_end_range, " .
                "asn_number, asn_country, asn_description, asn_offender_count) VALUES('" . $row['que_ip_adr'] . "/32', '" . $row['que_ip_adr'] .
                "', '" . $row['que_ip_adr'] . "', INET_ATON('" . $row['que_ip_adr'] . "'), INET_ATON('" . $row['que_ip_adr'] . "'), 999999, " .
                "'', '**NO ASN FOUND FOR THIS ADDRESS**', 1);";
              if (!$result2 = $db->query($SQL)) {
                die('There was an error running the query [' . $db->error . '] ' . $SQL);
              }
            }
          }
          $last_queue_id = $row['que_id'];
          $records_processed++;
/*        show_status($records_processed, $max_records); */
        }
      }
      mysqli_free_result($result);
      $SQL = "INSERT into offenders_process (proc_last_queue_id, proc_start_time, proc_records_done) VALUES (" .
        $last_queue_id . ", '" . $start_time . "', " . $records_processed . ");";
      if (!$result = $db->query($SQL)) {
        die('There was an error running the query [' . $db-> error . '] ' . $SQL);
      }
    }
    $db->close();
?>
process_offenders.sh - place in /root
#!/bin/bash
# Check if this script is already running, if not, proceed
if [ ! -f /tmp/process_offenders ]
  then
  echo "Processing" >> /tmp/process_offenders
  /usr/bin/php /usr/local/bin/offenders_by_asn.php 
  rm /tmp/process_offenders
fi
crontab entry to run offenders_by_asn on a regular basis (every 15 minutes)
*/15 * * * * /root/process_offenders.sh
offenders_by_classc.php - place in /usr/local/bin. Execute from the command line with "php /usr/local/bin/offenders_by_classc.php".
<?php

/* Set your specific configuration below */
$user_name = "xxxxx";
$password = "password";
$database = "snorby";
$server = "localhost";
$total_records = 14461947;
$records_processed = 0;
$cluster_limit = 16; /* Number of addresses in Class C range that constitute a cluster */
$ranges_to_skip = array(ip2long("0.0.0.0"), ip2long("0.255.255.255"),
                        ip2long("10.0.0.0"), ip2long("10.255.255.255"),
                        ip2long("100.64.0.0"), ip2long("100.127.255.255"),
                        ip2long("127.0.0.0"), ip2long("127.255.255.255"),
                        ip2long("169.254.0.0"), ip2long("169.254.255.255"),
                        ip2long("172.16.0.0"), ip2long("172.31.255.255"),
                        ip2long("192.0.0.0"), ip2long("192.0.0.255"),
                        ip2long("192.0.2.0"), ip2long("192.0.2.255"),
                        ip2long("192.88.99.0"), ip2long("192.88.99.255"),
                        ip2long("192.168.0.0"), ip2long("192.168.255.255"),
                        ip2long("198.18.0.0"), ip2long("198.19.255.255"),
                        ip2long("198.51.100.0"), ip2long("198.51.100.255"),
                        ip2long("203.0.113.0"), ip2long("203.0.113.255"),
                        ip2long("224.0.0.0"), ip2long("239.255.255.255"),
                        ip2long("240.0.0.0"), ip2long("255.255.255.255"));

header('Content-Type: text/plain');

function show_status($done, $total, $size=30) {

    static $start_time;

    // if we go over our bound, just ignore it
    if($done > $total) return;
    if(empty($start_time)) $start_time=time();
    $now = time();
    $perc=(double)($done/$total);
    $bar=floor($perc*$size);
    $status_bar="\r[";
    $status_bar.=str_repeat("=", $bar);
    if($bar<$size){
        $status_bar.=">";
        $status_bar.=str_repeat(" ", $size-$bar);
    } else {
        $status_bar.="=";
    }
    $disp=number_format($perc*100, 0);
    $status_bar.="] $disp%  $done/$total";
    $rate = ($now-$start_time)/$done;
    $left = $total - $done;
    $eta = round($rate * $left, 2);
    $elapsed = $now - $start_time;
    $status_bar.= " remaining: ".number_format($eta)." sec.  elapsed: ".number_format($elapsed)." sec.";
    echo "$status_bar  ";
    flush();

    // when done, send a newline
    if($done == $total) {
        echo "\n";
    }

}

function v4CIDRtoMask($cidr) {
    $cidr = explode('/', $cidr);
    return array($cidr[0], long2ip(-1 << (32 - (int)$cidr[1])));
}

function ipv4Breakout ($ip_address, $ip_nmask) {
    //convert ip addresses to long form
    $ip_address_long = ip2long($ip_address);
    $ip_nmask_long = ip2long($ip_nmask);

    //caculate network address
    $ip_net = $ip_address_long & $ip_nmask_long;

    //caculate first usable address
    $ip_host_first = ((~$ip_nmask_long) & $ip_address_long);
    $ip_first = ($ip_address_long ^ $ip_host_first) + 1;

    //caculate last usable address
    $ip_broadcast_invert = ~$ip_nmask_long;
    $ip_last = ($ip_address_long | $ip_broadcast_invert) - 1;

    //caculate broadcast address
    $ip_broadcast = $ip_address_long | $ip_broadcast_invert;

    $block_info = array(array("network" => "$ip_net"),
            array("first_host" => "$ip_first"),
            array("last_host" => "$ip_last"),
            array("broadcast" => "$ip_broadcast"));

    return $block_info;
}

function rangeNotExcluded ($ip_address, $ranges_to_skip, &$next_address) {
    $continue = true;
    $status = true;
    $i = 0;
    do {
      if ($ip_address >= $ranges_to_skip[$i] and $ip_address <= $ranges_to_skip[$i+1]) { 
        $continue = false;
        $status = false;
        $next = long2ip($ranges_to_skip[$i+1]);
        $next_address = explode(".", $next);
      } else {
        $i = $i + 2;
        if ($i+1 > count($ranges_to_skip)) { 
          $continue = false;
        }
      }
    } while ($continue);
    return $status;
}

/* Connect to database, if unsuccessful keep trying for 100 seconds */
    $i = 0;
    while ( $i < 100 ) {
      $db = new mysqli($server, $user_name, $password, $database);
      if ($db->connect_errno > 0) {
        print('Unable to connect to database [' . $db->connect_error . ']');
        sleep(10);
        $i = $i + 10;
      }
      else {
        $i = 100;
      }
    }

/* Main program */

  $SQL = "TRUNCATE TABLE offender_address;";
  if (!$result = $db->query($SQL)) {
    die('There was an error running the query [' . $db->error . '] ' . $SQL);
  }
  $SQL = "TRUNCATE TABLE offenders_by_classc;";
  if (!$result = $db->query($SQL)) {
    die('There was an error running the query [' . $db->error . '] ' . $SQL);
  }
  $SQL = "INSERT INTO offender_address (oa_ip_adr, oa_ip_numeric) SELECT distinct(que_ip_adr), INET_ATON(que_ip_adr) " . 
    "FROM `block_queue` ORDER BY INET_ATON(que_ip_adr)";
  if (!$result = $db->query($SQL)) {
    die('There was an error running the query [' . $db->error . '] ' . $SQL);
  }

  /* loop through all Class C address ranges, looking for clusters matching the criteria */
  $next_address = array("0","0","0","0");
  for ($a=1; $a <= 255; $a++) {
    for ($b=0; $b <= 255; $b++) {
      for ($c=0; $c <= 255; $c++) {
        $this_range = $a . '.' . $b . '.' . $c . '.0/24';
        $ipResult = v4CIDRtoMask($this_range);
        $blockInfo = ipv4Breakout($ipResult[0], $ipResult[1]);
        if (rangeNotExcluded($blockInfo[1]["first_host"], $ranges_to_skip, $next_address)) {
          $SQL = "SELECT count(*) as hits from offender_address WHERE oa_ip_numeric BETWEEN INET_ATON('" . long2ip($blockInfo[1]["first_host"]) . 
            "') AND INET_ATON('" . long2ip($blockInfo[2]["last_host"]) . "');";
          if (!$result = $db->query($SQL)) {
            die('There was an error running the query [' . $db->error . '] ' . $SQL);
          }
          if ($result->num_rows > 0) {
            $row = $result->fetch_assoc();
            if ($row["hits"] >= $cluster_limit) {
              $SQL = "INSERT INTO offenders_by_classc (clc_bgp_prefix, clc_start_address, clc_end_address, clc_start_range, clc_end_range, " .
                "clc_offender_count) VALUES('" . $this_range . "', '" . long2ip($blockInfo[1]["first_host"]) .
                "', '" . long2ip($blockInfo[2]["last_host"]) . "', INET_ATON('" . long2ip($blockInfo[1]["first_host"]) . "'), INET_ATON('" .
                long2ip($blockInfo[2]["last_host"]) . "'), 1);";
              if (!$result = $db->query($SQL)) {
                die('There was an error running the query [' . $db->error . '] ' . $SQL);
              }
            }
          }
        } else {
          $a = (int) $next_address[0]; 
          $b = (int) $next_address[1]; 
          $c = (int) $next_address[2];
        }
      $records_processed++;
      show_status($records_processed, $total_records);
      }
    }
  }
  /* Count the number of events blocked in each address range */
  $SQL = "UPDATE offenders_by_classc SET clc_offender_count = (SELECT count(*) FROM block_queue where INET_ATON(que_ip_adr) " . 
    "BETWEEN clc_start_range AND clc_end_range);";
  if (!$result = $db->query($SQL)) {
    die('There was an error running the query [' . $db->error . '] ' . $SQL);
   }
  $db->close();
?>
top_offenders_block.php - Place in /usr/local/bin, run from the command line with "php /usr/local/bin/top_offenders_block.php"
<?php
require('routeros_api.class.php');

/* Set your specific configuration below */
$user_name = "xxxxx";
$password = "password";
$database = "snorby";
$server = "localhost";
$mikrotik_addr = "192.168.x.x";
$mikrotik_user = "admin";
$mikrotik_pwd = "secret-password";

header('Content-Type: text/plain');

$API = new RouterosAPI();

function AddToFirewall($thisrow, $bgp_prefix_field, $delete) {

  global $API, $mikrotik_addr, $mikrotik_user, $mikrotik_pwd;

  try {
      $API->connect($mikrotik_addr, $mikrotik_user, $mikrotik_pwd);
  } catch (Exception $e) {
      die('Unable to connect to RouterOS. Error:' . $e);
  }
  if ($delete) { 
    $ARRAY = $API->comm("/ip/firewall/address-list/print", array(
       ".proplist"=> ".id",
       "?list" => "Perm Blocked",));
    foreach ($ARRAY as $a) {
      foreach ($a as $name => $value) {
        $API->write("/ip/firewall/address-list/remove",false);
        $API->write("=.id=$value",true);
        $API->read();
      }
    }
  }
  $API->comm("/ip/firewall/address-list/add", array(
    "list" => "Perm Blocked",
    "address" => $thisrow[$bgp_prefix_field],));
  $API->disconnect();
  return true;
}

function processClassC($thisdb) {
  $SQL = "SELECT count(*) as hits, sum(clc_offender_count) as tothits, sum((clc_end_range - clc_start_range) + 1) as address_count " .
    "from offenders_by_classc where clc_is_excluded = 0;";
  if(!$result = $thisdb->query($SQL)) {
    die('There was an error running the query [' . $thisdb->error . '] ' . $SQL);
  }
  if ($result->num_rows > 0) {
    $row = $result->fetch_assoc();
    echo "\r\nUsing the Class C analysis method to create firewall rules.\r\n";
    echo "\r\nThere are " . $row['hits'] . " entries in the Class C offenders table.\r\n";
    echo $row['address_count'] . " hosts will be blocked, stopping approximately ";
    $SQL2 = "SELECT count(*) as hits from block_queue;";
    if (!$result2 = $thisdb->query($SQL2)) {
      die('There was an error running the query [' . $thisdb->error . '] ' . $SQL2);
    }
    $row2 = $result2->fetch_assoc();
    $percent = number_format((float)($row['tothits'] / $row2['hits'] * 100), 2, '.', '');
    echo $percent . "% of the events from entering your network.\r\n\r\n";
    $continue = false;
    do {
      echo "Proceed with creating firewall rules based on Class C analysis? ";
      $proceed = strtolower(readline("[y/n]: "));
      if ($proceed == "y") {
        $continue = true;
      } elseif ($proceed == "n") {
        $continue = true;
      } else {
        echo "Invalid selection.  Select y or n.\r\n";
      }
    } while ($continue == false);
    if ($proceed == "y") {
      $SQL = "SELECT * from offenders_by_classc where clc_is_excluded = 0;";
      if (!$result = $thisdb->query($SQL)) {
        die('There was an error running the query [' . $thisdb->error . '] ' . $SQL);
      }
      if ($result->num_rows > 0) {
        $delete = true;
        while($row = $result->fetch_assoc()) {
          AddToFirewall($row, 'clc_bgp_prefix', $delete);
          $delete = false;
          $SQL2 = "UPDATE offenders_by_classc SET clc_is_blocked = 1 WHERE clc_id = " . $row['clc_id'] . ";";
          if (!$result2 = $thisdb->query($SQL2)) {
             die('There was an error running the query [' . $thisdb->error . '] ' . $SQL2);
          }
        }
        mysqli_free_result($result);
      }
    }
  }   
}

function processASN($thisdb) {
  $SQL = "SELECT count(*) as hits from offenders_by_asn where asn_is_excluded = 0;"; 
  if(!$result = $thisdb->query($SQL)) {
    die('There was an error running the query [' . $thisdb->error . '] ' . $SQL);
  }
  if ($result->num_rows > 0) {
    $row = $result->fetch_assoc();
    echo "\r\nUsing the ASN analysis method to create firewall rules.\r\n";
    echo "\r\nThere are " . $row['hits'] . " entries in the ASN offenders table.\r\n";
    $continue = false;
    do {
      $rowcount = 100;
      echo "How many of the top offenders records should be used? ";
      $proceed = strtolower(readline("[Default 100/number]: "));
      if ($proceed == "") {
        $continue = true;
      } elseif (is_numeric($proceed)) {
        $continue = true;
        $rowcount = (int) $proceed;
      } else {
        echo "Invalid entry. Please enter a number.\r\n";
      }
    } while ($continue == false);
    $SQL = "SELECT sum(asn_offender_count) as tothits FROM (SELECT a.asn_offender_count FROM offenders_by_asn a WHERE asn_is_excluded = 0 " .  
      "ORDER BY asn_offender_count DESC LIMIT " . $rowcount . ") as b;";
    if(!$result = $thisdb->query($SQL)) {
      die('There was an error running the query [' . $thisdb->error . '] ' . $SQL);
    }
    if ($result->num_rows > 0) {
      $row = $result->fetch_assoc();
      $SQL2 = "SELECT sum(addresses) as address_count FROM (SELECT (a.asn_end_range - a.asn_start_range) + 1 as addresses " .   
        "FROM offenders_by_asn a WHERE asn_is_excluded = 0 ORDER BY asn_offender_count DESC LIMIT " . $rowcount . ") as b;";
      if(!$result = $thisdb->query($SQL2)) {
        die('There was an error running the query [' . $thisdb->error . '] ' . $SQL2);
      }
      if ($result->num_rows > 0) {
        $rowb = $result->fetch_assoc();
        echo $rowb['address_count'] . " hosts will be blocked, stopping approximately ";
        $SQL2 = "SELECT count(*) as hits from block_queue;";
        if (!$result2 = $thisdb->query($SQL2)) {
          die('There was an error running the query [' . $thisdb->error . '] ' . $SQL2);
        }
        $row2 = $result2->fetch_assoc();
        $percent = number_format((float)($row['tothits'] / $row2['hits'] * 100), 2, '.', '');
        echo $percent . "% of the events from entering your network.\r\n\r\n";
      }
      $continue = false;
      do {
        echo "Proceed with creating firewall rules based on ASN analysis? ";
        $proceed = strtolower(readline("[y/n]: "));
        if ($proceed == "y") {
          $continue = true;
        } elseif ($proceed == "n") {
          $continue = true;
        } else {
          echo "Invalid selection.  Select y or n.\r\n";
        }
      } while ($continue == false);
      if ($proceed == "y") {
        $SQL = "SELECT * from offenders_by_asn where asn_is_excluded = 0 ORDER BY asn_offender_count LIMIT " . $rowcount . ";";
        if (!$result = $thisdb->query($SQL)) {
          die('There was an error running the query [' . $thisdb->error . '] ' . $SQL);
        }
        if ($result->num_rows > 0) {
          $delete = true;
          while($row = $result->fetch_assoc()) {
            AddToFirewall($row, 'asn_bgp_prefix', $delete);
            $delete = false;
            $SQL2 = "UPDATE offenders_by_asn SET asn_is_blocked = 1 WHERE asn_id = " . $row['asn_id'] . ";";
            if (!$result2 = $thisdb->query($SQL2)) {
               die('There was an error running the query [' . $thisdb->error . '] ' . $SQL2);
            }
          }
          mysqli_free_result($result);
        }
      }
    }
  }
}

/* $API->debug = true; */

/* Connect to database, if unsuccessful keep trying for 100 seconds */
  $i = 0;
  while ( $i < 100 ) {
    $db = new mysqli($server, $user_name, $password, $database);
    if ($db->connect_errno > 0) {
      print('Unable to connect to database [' . $db->connect_error . ']');
      sleep(10);
      $i = $i + 10;
    }
    else {
      $i = 100;
    }
  }

/* Main program loop */
  $has_ASN = false;
  $has_ClassC = false;
  echo "Block Top Offenders ";
  echo "by Tom Fisk\r\n\r\n";
  echo "This program will create an address list on a Mikrotik router to block address ranges\r\n";
  echo "from either the ASN or Class C analysis methods.\r\n\r\n";
  $SQL = "SELECT count(*) as hits from offenders_by_asn;";
  if(!$result = $db->query($SQL)) {
    die('There was an error running the query [' . $db->error . '] ' . $SQL);
  }
  if ($result->num_rows > 0) {
    $row = $result->fetch_assoc();
    if ($row['hits'] > 0) { $has_ASN = true; } 
  }
  $SQL = "SELECT count(*) as hits from offenders_by_classc;";
  if(!$result = $db->query($SQL)) {
    die('There was an error running the query [' . $db->error . '] ' . $SQL);
  }
  if ($result->num_rows > 0) {
    $row = $result->fetch_assoc();
    if ($row['hits'] > 0) { $has_ClassC = true; }
  }
  if ($has_ASN && $has_ClassC) {
    $continue = false;
    do {
      echo "Which analysis type do you want to use to create the firewall rules?\r\n";
      $process_type = strtolower(readline("ASN or Class C [a/c]: "));
      if ($process_type == "a") {
        $continue = true;
      } elseif ($process_type == "c") {
        $continue = true;
      } else {
        echo "Invalid selection.  Select a or c.\r\n";
      }
    } while ($continue == false);
  } elseif ($has_ASN) {
   $process_type = "a";
  } else {
   $process_type = "c";
  }
  if ($process_type == "a") {
    processASN($db);
  } elseif ($process_type == "c") {
    processClassC($db);
  } else {
    die("You shouldn't get here!");
  }
  $db->close();
?>