Build a Threat Intelligence Lookup Tool
Query real security APIs to investigate IP addresses and generate threat reports.
Introduction
30 Second Summary
When a suspicious login attempt hits a company's servers, security engineers don't just guess whether the source is dangerous. They look up the IP address against global threat databases to make an informed decision in seconds.
In this project, you will build a Python command-line tool that queries two real-world threat intelligence APIs to generate a risk report for any IP address. You will learn how to authenticate with APIs, parse JSON responses, and handle errors.
What You'll Build
A Python command-line tool called IP Threat Checker that takes an IP address as input and queries VirusTotal and AbuseIPDB to generate a consolidated threat intelligence report.
By the end of this project, you'll have:
- A Python script (threat_checker.py) that makes authenticated API requests to VirusTotal and AbuseIPDB.
- Reusable functions for each API call with proper error handling for bad keys, rate limits, and network failures.
- A formatted threat intelligence report displayed in the terminal, combining data from multiple sources into a single risk assessment.
- Secret Mission: Extend your tool to also look up domain names, so it can investigate both IPs and domains.
Are there any prerequisites?
You should be comfortable with basic Python concepts like variables, functions, conditionals, and loops. No prior API experience is needed.
Set Up Python and Your Project
Your threat checker tool is a Python script that queries security APIs. Before you can write any code, you need Python installed on your machine.
In this step, you'll install Python, create an isolated project environment, and run a quick test to confirm everything works.
In this step, get ready to:
- Install Python 3.14 and verify it works.
- Create a project directory with a virtual environment.
- Run a test script to confirm your setup.
Install Python 3.14
Python is the programming language your threat checker runs on. Let's check if you already have it installed.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check if Python is already installed by running this command:
python3 --version
✔️ I see a version number
You're all set. Make sure your version is 3.12 or higher, which is required for this project's virtual environment setup.
ⓧ Command not found
No worries. Let's install Python from the official website.
- Open your browser and go to the Python downloads page.
- Click the Download Python 3.14.4 button.
macOS
- Open the downloaded .pkg file.
- Follow the installer prompts and click through each step.
- After the installation finishes, open Finder.
- Navigate to Applications, then open the Python 3.14 folder.
- Double-click Install Certificates.command. A Terminal window will open briefly to install the certificates.
What does Install Certificates do?
This command installs SSL root certificates that Python needs to make HTTPS requests. Without it, your API calls to VirusTotal and AbuseIPDB would fail with certificate errors.
Wait until you see Successfully installed certifi and update complete in the Terminal window, then close it.
Windows
- Open the downloaded .exe file.
- Check the Add python.exe to PATH checkbox at the bottom of the installer.
- Click Install Now.
- Follow the prompts to complete the installation.
- Close and reopen your Terminal (or PowerShell), then verify the installation by running:
python3 --version
You should see Python 3.14.4 (or a similar version number) in the output.
Still seeing 'command not found'?
- On macOS, make sure you downloaded the macOS universal2 installer from python.org, not a source tarball.
- On Windows, make sure you checked Add python.exe to PATH during installation. If you missed it, run the installer again and select Modify.
- On Windows, try running python --version (without the 3) instead.
- Help me troubleshoot my Python installation.
Create your project directory and virtual environment
Python projects need an isolated space to manage their packages. Let's create a project folder and set up that isolation now.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir threat-checker
cd threat-checker
- Create a virtual environment inside your project by running:
python3 -m venv venv
What is a virtual environment?
A virtual environment is a self-contained folder that holds its own copy of Python and any packages you install. This keeps your project's dependencies separate from other projects on your computer.
Starting with Python 3.12, you cannot install packages globally with pip. A virtual environment is required.
- Activate the virtual environment by running:
macOS
source venv/bin/activate
Windows
venv\Scripts\Activate.ps1
Seeing a permissions error?
If PowerShell blocks the activation script, run this command first, then try activating again:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
You should see (venv) appear at the start of your terminal prompt. This means the virtual environment is active.
Don't see (venv) in your prompt?
- Make sure you ran the activation command from inside the threat-checker folder where you created the virtual environment.
- Try running ls (macOS) or dir (Windows) and confirm you see a venv folder in the output.
- Help me fix my virtual environment activation.
- Install the requests library by running:
pip install requests==2.33.1
What is the requests library?
The requests library makes it easy to send HTTP requests from Python. You'll use it to talk to the VirusTotal and AbuseIPDB APIs later in this project.
You should see Successfully installed requests-2.33.1 (along with its dependencies) in the terminal output.
Seeing an error when installing?
- Make sure your virtual environment is active. You should see (venv) in your terminal prompt before running pip install.
- If you see a "no module named pip" error, try running python3 -m ensurepip first, then retry the install.
- Help me fix my pip installation error.
Create your project file and test it
Now that your environment is ready, let's open the project in VS Code and create your first file.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Select the threat-checker folder on your Desktop, then click Open.
- In the Explorer sidebar on the left, click the New File icon (a page with a plus sign).
- Name the file threat_checker.py and press Enter.
- Add this test code to the file:
print("Hello, threat checker!")
What does this code do?
This is a simple test to confirm Python can run a script from your project folder. If this prints successfully, your environment is correctly set up for the rest of the project.
- Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Back in your Terminal, make sure you are still inside the threat-checker folder with (venv) active.
- Run your script by entering this command:
python3 threat_checker.py
You should see Hello, threat checker! printed in the terminal.
Not seeing the expected output?
- Make sure you saved threat_checker.py before running the command.
- Check that your terminal is in the threat-checker folder. Run pwd to see your current directory.
- If you see "No such file or directory", the file might be saved in a different location. Check that threat_checker.py appears in the VS Code Explorer sidebar at the top level of threat-checker.
- Help me troubleshoot running my Python script.
✔️ Awesome, I've got everything!
Make sure you've saved threat_checker.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for threat_checker.py at this point:
print("Hello, threat checker!")
Your development environment is ready to go. Next up, you'll grab the API keys that let your tool talk to VirusTotal and AbuseIPDB.
Get Your API Keys
Your Python environment is set up and ready to go. Now you need credentials to talk to the two threat intelligence services your tool depends on.
APIs use API keys to verify who is making each request. In this step, you will create free accounts on VirusTotal and AbuseIPDB, grab your keys, and store them in a dedicated config file.
In this step, get ready to:
- Create a free VirusTotal account and copy your API key.
- Create a free AbuseIPDB account and copy your API key.
- Store both keys in a config file and import them into your script.
Sign up for VirusTotal and copy your API key
VirusTotal aggregates results from 70+ antivirus engines to tell you whether an IP address has been flagged as malicious. Its free tier gives you 500 requests per day and 4 requests per minute.
- Open your browser and go to virustotal.com.
- Click Sign In in the top-right corner of the page.
- Click Join us today to create a free VirusTotal Community account.
- Fill in your name, email, and a password, then click Join us.
- Confirm your email address if prompted.
Now that your account is created, you can grab your API key.
- Click your profile icon in the top-right corner of the page.
- Select API key from the dropdown menu.
- Copy the long string of letters and numbers displayed on the page. This is your VirusTotal API key.
- Paste it somewhere safe for now (a text file or your clipboard).
Can't find the menu?
You can also go directly to virustotal.com/gui/my-apikey while logged in to see your key.
What is an API key?
An API key is a unique string that identifies you to a service. Every time your script sends a request to VirusTotal, it includes this key so VirusTotal knows who is asking.
Treat your API key like a password. Anyone who has it can make requests on your behalf and use up your daily quota.
Sign up for AbuseIPDB and copy your API key
AbuseIPDB is a crowdsourced database of IP addresses reported for abusive activity. Its free tier gives you 1,000 requests per day.
- Open your browser and go to abuseipdb.com.
- Click Register (or Sign Up) in the top-right corner.
- Fill in your email and a password, then submit the form.
- Confirm your email address if prompted.
- Log in with the account you just created.
With your account ready, navigate to the API key page.
- Go to your account page by clicking your username in the top-right corner.
- Click the API tab in the navigation menu.
- Click Create Key if you do not already have one.
- Copy the API key string and paste it somewhere safe alongside your VirusTotal key.
Didn't get the confirmation email?
Check your spam folder first. Some email providers block AbuseIPDB emails via DNS-based blocklists. If you still do not see it, use the Resend Confirmation link on the login page.
Help me troubleshoot my AbuseIPDB signup
Why two different APIs?
VirusTotal checks an IP against 70+ antivirus engines for malware and reputation signals. AbuseIPDB collects crowdsourced abuse reports from network administrators worldwide.
Combining both gives you a much fuller picture than either source alone. Real security teams layer multiple intelligence feeds for the same reason.
Create a config file for your API keys
Hardcoding secrets directly into your main script is a bad habit. If you ever share your code, your keys go with it.
The simplest fix is to store keys in a separate file and import them. This keeps secrets out of your application logic.
- In VS Code, create a new file in your threat-checker folder called config.py.
- Add the following code to config.py, replacing the placeholder strings with your actual API keys:
# Replace these with your actual API keys
VIRUSTOTAL_API_KEY = "your-virustotal-api-key-here"
ABUSEIPDB_API_KEY = "your-abuseipdb-api-key-here"
What does this code do?
Each line creates a Python variable holding one of your API keys as a string. Other files in your project can import these variables without needing to know the actual key values.
- Save config.py.
- Confirm the file appears in the VS Code Explorer sidebar alongside threat_checker.py.
Never share your API keys
If you ever push this project to GitHub, add config.py to your .gitignore file so your keys are not exposed publicly.
Now update your main script to import the keys from your new config file.
- Open threat_checker.py in VS Code.
- Select all existing code and replace it with the following:
from config import VIRUSTOTAL_API_KEY, ABUSEIPDB_API_KEY
print("API keys loaded successfully!")
What does this code do?
The from config import line pulls VIRUSTOTAL_API_KEY and ABUSEIPDB_API_KEY from your config.py file into your main script. The print statement confirms the import worked without errors.
- Save threat_checker.py.
- Run the script by entering this command in your terminal:
python3 threat_checker.py
- You should see API keys loaded successfully! printed in the terminal.
Seeing ModuleNotFoundError?
If you see ModuleNotFoundError: No module named 'config', make sure config.py is in the same folder as threat_checker.py. Also check that your terminal is inside the threat-checker folder and your virtual environment is still active (you should see (venv) at the start of your terminal prompt).
Help me fix my config import error
✔️ Awesome, I've got everything!
Great. Double check you have saved both config.py and threat_checker.py.
ⓧ I'd like to double check the full code
Here is the full code for both files at this point.
# Replace these with your actual API keys
VIRUSTOTAL_API_KEY = "your-virustotal-api-key-here"
ABUSEIPDB_API_KEY = "your-abuseipdb-api-key-here"
from config import VIRUSTOTAL_API_KEY, ABUSEIPDB_API_KEY
print("API keys loaded successfully!")
Your credentials are in place and your script can reach them. Next up, you will use these keys to send your first real request to the VirusTotal API and pull back live threat data.
Query the VirusTotal API
Your API keys are loaded and your project is ready to make its first real request. In this step, you will write a function that talks to the VirusTotal API and asks it what it knows about a given IP address.
VirusTotal aggregates scan results from dozens of security engines. When you query an IP address, the API returns verdicts from each engine along with geographic and network data. Your function will send the request, read the response, and pull out the fields that matter.
In this step, get ready to:
- Write a function that sends an authenticated request to the VirusTotal API.
- Parse the JSON response to extract threat intelligence data.
- Add error handling for common API issues like invalid keys and rate limits.
Update your imports
Before writing the function, you need two new imports. The requests library handles HTTP communication with the API. The sys module provides access to command-line arguments, which you will use in a later step.
- In VS Code, open threat_checker.py from the Explorer sidebar.
- Find the existing code at the top of the file:
from config import VIRUSTOTAL_API_KEY, ABUSEIPDB_API_KEY
print("API keys loaded successfully!")
- Replace the entire contents of the file with these three import lines:
import requests
import sys
from config import VIRUSTOTAL_API_KEY, ABUSEIPDB_API_KEY
What changed?
You added two new imports at the top and removed the test print statement. The print line was only there to confirm your keys loaded. It is no longer needed now that you are building real functionality.
Write the function and make the API request
Every REST API call has two parts: where to send the request, and how to prove you are allowed to use it. VirusTotal uses an API key passed in an HTTP header called x-apikey.
- Below the import lines in threat_checker.py, add a blank line and then paste the beginning of the check_virustotal function:
def check_virustotal(ip_address):
"""Query VirusTotal API v3 for IP address threat data."""
# Build the request URL by inserting the target IP
url = f"https://www.virustotal.com/api/v3/ip_addresses/{ip_address}"
# Authenticate by passing your API key in the x-apikey header
headers = {
"x-apikey": VIRUSTOTAL_API_KEY
}
try:
# Send a GET request to the VirusTotal API
response = requests.get(url, headers=headers)
What does this code do?
- url builds the VirusTotal endpoint by inserting the IP address directly into the URL path. This is a common REST API pattern.
- headers is a dictionary containing your API key. VirusTotal expects this key in a header named x-apikey.
- requests.get() sends an HTTP GET request. The try block wraps the call so network failures do not crash your script.
What is API authentication?
API authentication proves your identity to a service. Different APIs use different methods. VirusTotal uses a header-based key, meaning you include your secret key in the HTTP headers of every request.
This is one of the most common authentication patterns you will encounter when working with APIs.
Your function can now send the request. The next piece is reading the response.
Parse the JSON response
The VirusTotal API returns its data as JSON, a structured format that Python can convert into a dictionary. The response contains analysis verdicts from dozens of security engines, plus geographic and network info about the IP.
- Directly below the response = requests.get(url, headers=headers) line, add the following code to handle a successful response. Make sure the indentation stays inside the try block:
if response.status_code == 200:
# Convert the JSON response into a Python dictionary
data = response.json()
attributes = data["data"]["attributes"]
# Extract the specific fields we care about
result = {
"malicious": attributes["last_analysis_stats"]["malicious"],
"suspicious": attributes["last_analysis_stats"]["suspicious"],
"harmless": attributes["last_analysis_stats"]["harmless"],
"undetected": attributes["last_analysis_stats"]["undetected"],
"country": attributes.get("country", "Unknown"),
"as_owner": attributes.get("as_owner", "Unknown"),
"reputation": attributes.get("reputation", 0),
}
return result
What does this code do?
- response.status_code == 200 checks that the API returned a success status. HTTP status code 200 means "OK".
- response.json() converts the raw JSON text into a Python dictionary so you can access fields with bracket notation.
- last_analysis_stats contains the count of security engines that flagged the IP as malicious, suspicious, harmless, or undetected.
- attributes.get("country", "Unknown") safely retrieves a field with a fallback value. If the field is missing from the response, it returns "Unknown" instead of crashing.
The function can now read a successful response. The last piece is handling what happens when something goes wrong.
Add error handling
API calls can fail for many reasons. Your API key might be wrong, you might hit a rate limit, or the network might drop. Each of these problems returns a different HTTP status code. Handling these codes gracefully keeps your tool from crashing with a confusing traceback.
- Directly below the return result line (still inside the try block), add the error handling code:
elif response.status_code == 401:
print("[VirusTotal] Error: Invalid API key.")
return None
elif response.status_code == 429:
print("[VirusTotal] Error: Rate limit exceeded. Try again later.")
return None
else:
# Catch any other unexpected status code
print(f"[VirusTotal] Error: Unexpected status code {response.status_code}")
return None
except requests.exceptions.RequestException as e:
# Handle network-level errors (no internet, DNS failure, timeout)
print(f"[VirusTotal] Network error: {e}")
return None
What does this code do?
- Status code 401 means "Unauthorized." This fires when your API key is incorrect or missing.
- Status code 429 means "Too Many Requests." VirusTotal's free tier allows 4 requests per minute. If you exceed this, the API tells you to slow down.
- The else branch catches any other status code you did not explicitly handle.
- The except requests.exceptions.RequestException block catches network-level failures like a lost internet connection or a DNS lookup failure. These are different from API errors because the request never reaches VirusTotal at all.
- Save threat_checker.py.
- Test the function by running this command in your terminal (make sure your virtual environment is active):
python3 -c "from threat_checker import check_virustotal; print(check_virustotal('8.8.8.8'))"
You should see a dictionary printed in your terminal with keys like malicious, harmless, country, and as_owner. The IP 8.8.8.8 is Google's public DNS server, so it should show 0 malicious verdicts.
Seeing an error instead of a dictionary?
- If you see [VirusTotal] Error: Invalid API key., open config.py and make sure you replaced the placeholder with your actual VirusTotal API key. The key should not have any extra spaces or quotes around it.
- If you see ModuleNotFoundError: No module named 'requests', your virtual environment may not be active. Look for (venv) at the start of your terminal prompt. If it is missing, activate it by running source venv/bin/activate.
- If you see a SyntaxError or IndentationError, double check that all three code sections inside the function are indented correctly. The if, elif, and else branches should all align at 8 spaces (inside the try block).
Still stuck? Help me debug my VirusTotal API function
Use the tabs below to double check your complete file matches what you should have so far.
✔️ Awesome, I've got everything!
Make sure you have saved threat_checker.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete threat_checker.py file as it should look at the end of this step.
import requests
import sys
from config import VIRUSTOTAL_API_KEY, ABUSEIPDB_API_KEY
def check_virustotal(ip_address):
"""Query VirusTotal API v3 for IP address threat data."""
url = f"https://www.virustotal.com/api/v3/ip_addresses/{ip_address}"
headers = {
"x-apikey": VIRUSTOTAL_API_KEY
}
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
attributes = data["data"]["attributes"]
result = {
"malicious": attributes["last_analysis_stats"]["malicious"],
"suspicious": attributes["last_analysis_stats"]["suspicious"],
"harmless": attributes["last_analysis_stats"]["harmless"],
"undetected": attributes["last_analysis_stats"]["undetected"],
"country": attributes.get("country", "Unknown"),
"as_owner": attributes.get("as_owner", "Unknown"),
"reputation": attributes.get("reputation", 0),
}
return result
elif response.status_code == 401:
print("[VirusTotal] Error: Invalid API key.")
return None
elif response.status_code == 429:
print("[VirusTotal] Error: Rate limit exceeded. Try again later.")
return None
else:
print(f"[VirusTotal] Error: Unexpected status code {response.status_code}")
return None
except requests.exceptions.RequestException as e:
print(f"[VirusTotal] Network error: {e}")
return None
Your config.py file should remain unchanged from the previous step.
Your tool can now query VirusTotal and extract meaningful threat data from a single API. Next up, you will connect to a second API to get a completely different perspective on the same IP address.
Query the AbuseIPDB API
Your check_virustotal function can now query VirusTotal, one of the world's largest malware-scanning databases. It's time to add a second data source to your tool.
AbuseIPDB is a crowdsourced database where network administrators report abusive IP addresses. By querying both APIs, your tool combines two independent threat signals for a more complete picture of any IP's risk level.
In this step, get ready to:
- Write the check_abuseipdb function to query the AbuseIPDB API.
- Parse the JSON response to extract abuse report data.
- Add error handling for common API failures.
Send a GET request to the AbuseIPDB API
AbuseIPDB's API works differently from VirusTotal in a couple of key ways. The IP address is sent as a query parameter instead of being embedded in the URL path. The authentication header is called Key instead of x-apikey.
- In threat_checker.py, scroll to the end of the check_virustotal function and add the following code below it:
def check_abuseipdb(ip_address):
"""Query AbuseIPDB API v2 for IP abuse reports."""
url = "https://api.abuseipdb.com/api/v2/check"
headers = {
"Key": ABUSEIPDB_API_KEY,
"Accept": "application/json"
}
params = {
"ipAddress": ip_address,
"maxAgeInDays": 90
}
try:
response = requests.get(url, headers=headers, params=params)
if response.status_code == 200:
data = response.json()
report = data["data"]
result = {
"abuse_confidence_score": report["abuseConfidenceScore"],
"total_reports": report["totalReports"],
"country_code": report.get("countryCode", "Unknown"),
"isp": report.get("isp", "Unknown"),
"domain": report.get("domain", "Unknown"),
"usage_type": report.get("usageType", "Unknown"),
"is_tor": report.get("isTor", False),
}
return result
elif response.status_code == 401:
print("[AbuseIPDB] Error: Invalid API key.")
return None
elif response.status_code == 429:
print("[AbuseIPDB] Error: Rate limit exceeded. Try again later.")
return None
elif response.status_code == 422:
print("[AbuseIPDB] Error: Invalid IP address format.")
return None
else:
print(f"[AbuseIPDB] Error: Unexpected status code {response.status_code}")
return None
except requests.exceptions.RequestException as e:
print(f"[AbuseIPDB] Network error: {e}")
return None
What does this code do?
- url points to a fixed endpoint. Unlike VirusTotal, the IP address is not embedded in the URL.
- headers sends two values: Key for authentication and Accept to tell the API you want JSON back.
- params passes the IP address and a 90-day lookback window as query parameters. The requests library appends these to the URL automatically.
- When the response is successful (status code 200), the function extracts seven fields from the JSON and returns them as a dictionary.
- The elif blocks handle specific HTTP error codes, and the except block catches network failures like a lost internet connection.
What are query parameters?
Query parameters are key-value pairs appended to a URL after a ? character. When you call requests.get(url, params=params), the library builds the full URL for you.
For example, your code produces a URL like https://api.abuseipdb.com/api/v2/check?ipAddress=8.8.8.8&maxAgeInDays=90. This is different from VirusTotal, where the IP was part of the URL path itself.
- Save threat_checker.py.
Understand the AbuseIPDB response fields
Your function extracts seven fields from the AbuseIPDB response. Each one tells you something different about the IP address.
What do the response fields mean?
- abuseConfidenceScore is a 0 to 100 rating of how likely the IP is to be abusive. Higher scores mean more reports from more sources.
- totalReports shows how many times the IP has been reported in the last 90 days (matching your maxAgeInDays setting).
- countryCode is the geographic location of the IP address.
- isp and domain identify the network provider hosting the IP.
- usageType describes how the IP is typically used, such as commercial, residential, or data center.
- isTor flags whether the IP is a known Tor exit node. Tor exit nodes are often associated with anonymized traffic.
Handle errors and test the function
The error handling follows the same pattern you used for VirusTotal. There is one new status code to watch for.
What does status code 422 mean?
HTTP status code 422 means the server understood your request but the data you sent is invalid. AbuseIPDB returns this when the IP address format is wrong, for example passing a domain name instead of an IP.
The other codes (401 for bad API key, 429 for rate limit) work the same way as VirusTotal.
- Test the function by running this command in your terminal:
python3 -c "from threat_checker import check_abuseipdb; print(check_abuseipdb('8.8.8.8'))"
- You should see a dictionary printed with keys like abuse_confidence_score, total_reports, country_code, and more.
Since 8.8.8.8 is Google's public DNS server, the abuse_confidence_score should be very low or zero.
Seeing an error?
- If you see [AbuseIPDB] Error: Invalid API key., double check that your API key in config.py matches the key from your AbuseIPDB dashboard exactly.
- If you see ModuleNotFoundError: No module named 'config', make sure you are running the command from inside the threat-checker folder where config.py lives.
- If you see a ConnectionError, check your internet connection and make sure your virtual environment is activated.
- Help me debug my AbuseIPDB API call
✔️ Awesome, I've got everything!
Great, double check you have saved threat_checker.py.
ⓧ I'd like to double check the full code
import requests
import sys
from config import VIRUSTOTAL_API_KEY, ABUSEIPDB_API_KEY
def check_virustotal(ip_address):
"""Query VirusTotal API v3 for IP address threat data."""
url = f"https://www.virustotal.com/api/v3/ip_addresses/{ip_address}"
headers = {
"x-apikey": VIRUSTOTAL_API_KEY
}
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
attributes = data["data"]["attributes"]
result = {
"malicious": attributes["last_analysis_stats"]["malicious"],
"suspicious": attributes["last_analysis_stats"]["suspicious"],
"harmless": attributes["last_analysis_stats"]["harmless"],
"undetected": attributes["last_analysis_stats"]["undetected"],
"country": attributes.get("country", "Unknown"),
"as_owner": attributes.get("as_owner", "Unknown"),
"reputation": attributes.get("reputation", 0),
}
return result
elif response.status_code == 401:
print("[VirusTotal] Error: Invalid API key.")
return None
elif response.status_code == 429:
print("[VirusTotal] Error: Rate limit exceeded. Try again later.")
return None
else:
print(f"[VirusTotal] Error: Unexpected status code {response.status_code}")
return None
except requests.exceptions.RequestException as e:
print(f"[VirusTotal] Network error: {e}")
return None
def check_abuseipdb(ip_address):
"""Query AbuseIPDB API v2 for IP abuse reports."""
url = "https://api.abuseipdb.com/api/v2/check"
headers = {
"Key": ABUSEIPDB_API_KEY,
"Accept": "application/json"
}
params = {
"ipAddress": ip_address,
"maxAgeInDays": 90
}
try:
response = requests.get(url, headers=headers, params=params)
if response.status_code == 200:
data = response.json()
report = data["data"]
result = {
"abuse_confidence_score": report["abuseConfidenceScore"],
"total_reports": report["totalReports"],
"country_code": report.get("countryCode", "Unknown"),
"isp": report.get("isp", "Unknown"),
"domain": report.get("domain", "Unknown"),
"usage_type": report.get("usageType", "Unknown"),
"is_tor": report.get("isTor", False),
}
return result
elif response.status_code == 401:
print("[AbuseIPDB] Error: Invalid API key.")
return None
elif response.status_code == 429:
print("[AbuseIPDB] Error: Rate limit exceeded. Try again later.")
return None
elif response.status_code == 422:
print("[AbuseIPDB] Error: Invalid IP address format.")
return None
else:
print(f"[AbuseIPDB] Error: Unexpected status code {response.status_code}")
return None
except requests.exceptions.RequestException as e:
print(f"[AbuseIPDB] Network error: {e}")
return None
Your tool can now query two independent threat intelligence sources. Next up, you will combine both results into a single consolidated threat report with a risk assessment.
Build the Consolidated Threat Report
Your two API functions are ready to fetch threat intelligence data from VirusTotal and AbuseIPDB. Right now, though, they each return a raw dictionary of data with no way to run them together.
In this step, you'll combine both API calls into a single report that prints a clean, readable summary in your terminal. You'll also add logic that interprets the results and flags the IP as dangerous or clean.
In this step, get ready to:
- Write a generate_report function that queries both APIs and formats the output.
- Add risk assessment logic that classifies IPs as dangerous or clean.
- Add a command-line interface so you can run the tool from Terminal.
Write the report display function
The generate_report function is the heart of your tool. It calls both API functions, receives their dictionaries of results, and prints each one in a neatly formatted section.
- In threat_checker.py, scroll to the bottom of the file (below the check_abuseipdb function).
- Add the following generate_report function:
def generate_report(ip_address):
"""Generate a consolidated threat intelligence report."""
print(f"\n{'='*55}")
print(f" THREAT INTELLIGENCE REPORT")
print(f" Target: {ip_address}")
print(f"{'='*55}\n")
# Query VirusTotal
print("[*] Querying VirusTotal...")
vt_result = check_virustotal(ip_address)
if vt_result:
print(f"\n--- VirusTotal Results ---")
print(f" Country: {vt_result['country']}")
print(f" Network Owner: {vt_result['as_owner']}")
print(f" Reputation: {vt_result['reputation']}")
print(f" Malicious: {vt_result['malicious']} engines")
print(f" Suspicious: {vt_result['suspicious']} engines")
print(f" Harmless: {vt_result['harmless']} engines")
print(f" Undetected: {vt_result['undetected']} engines")
else:
print(" VirusTotal lookup failed.")
# Query AbuseIPDB
print("\n[*] Querying AbuseIPDB...")
abuse_result = check_abuseipdb(ip_address)
if abuse_result:
print(f"\n--- AbuseIPDB Results ---")
print(f" Abuse Confidence Score: {abuse_result['abuse_confidence_score']}%")
print(f" Total Reports: {abuse_result['total_reports']}")
print(f" Country: {abuse_result['country_code']}")
print(f" ISP: {abuse_result['isp']}")
print(f" Domain: {abuse_result['domain']}")
print(f" Usage Type: {abuse_result['usage_type']}")
print(f" Tor Exit Node: {'Yes' if abuse_result['is_tor'] else 'No'}")
else:
print(" AbuseIPDB lookup failed.")
What does this code do?
This function orchestrates your entire tool. It takes an IP address and runs it through both APIs.
- The opening print lines create a header banner with the target IP address, separated by rows of = characters.
- check_virustotal(ip_address) and check_abuseipdb(ip_address) call the two functions you wrote in the previous steps.
- Each if/else block checks whether the API call succeeded. If the result is None (an error happened), it prints a failure message instead of crashing.
You won't see a change yet. The next substep adds the risk assessment section, then you'll wire it all up to run from the command line.
Add risk assessment logic
Displaying raw data is useful, but security engineers need a quick verdict. A risk assessment section interprets the combined results and gives a clear status.
- In threat_checker.py, find the last line of generate_report that says:
else:
print(" AbuseIPDB lookup failed.")
- Add the following risk assessment code directly below that else block, still inside the generate_report function (keep the same indentation level):
# Risk Assessment
print(f"\n{'='*55}")
print(f" RISK ASSESSMENT")
print(f"{'='*55}")
if vt_result and abuse_result:
is_dangerous = (
vt_result["malicious"] > 0
or abuse_result["abuse_confidence_score"] > 50
)
if is_dangerous:
print(" Status: POTENTIALLY DANGEROUS")
reasons = []
if vt_result["malicious"] > 0:
reasons.append(
f"{vt_result['malicious']} engines flagged as malicious"
)
if abuse_result["abuse_confidence_score"] > 50:
reasons.append(
f"Abuse confidence score: {abuse_result['abuse_confidence_score']}%"
)
for reason in reasons:
print(f" - {reason}")
else:
print(" Status: LIKELY CLEAN")
print(" No significant threats detected.")
else:
print(" Status: INCOMPLETE")
print(" Could not retrieve data from all sources.")
print(f"\n{'='*55}\n")
What does the risk assessment do?
This section combines data from both APIs into a single verdict.
- is_dangerous is True if VirusTotal flagged the IP as malicious OR if AbuseIPDB's abuse confidence score is above 50%.
- If dangerous, the reasons list collects the specific flags that triggered the warning, so the user knows exactly why.
- If either API call failed earlier (returned None), the status shows INCOMPLETE instead of guessing.
You won't see a change yet. The next substep adds the command-line interface so you can actually run the tool.
Add the command-line interface and test your tool
Python's if __name__ == "__main__" block runs code only when you execute the script directly from the terminal. This is where you'll read the IP address from the command-line arguments and kick off the report.
- In threat_checker.py, scroll to the very bottom of the file (below the generate_report function).
- Add the following __main__ block at the end of the file, with no indentation (it sits outside all functions):
if __name__ == "__main__":
# Check that the user provided an IP address argument
if len(sys.argv) != 2:
print("Usage: python3 threat_checker.py <ip_address>")
print("Example: python3 threat_checker.py 8.8.8.8")
sys.exit(1)
# Read the IP address from the command line and generate the report
target_ip = sys.argv[1]
generate_report(target_ip)
What does this code do?
- sys.argv is a list of arguments passed to the script. sys.argv[0] is the script name itself, and sys.argv[1] is the first argument (the IP address).
- if len(sys.argv) != 2 checks that the user typed exactly one argument. If they forgot the IP address, the script prints a usage example and exits.
- generate_report(target_ip) calls the function you just wrote, which triggers both API calls and prints the full report.
- Save threat_checker.py.
✔️ Awesome, I've got everything!
Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete threat_checker.py file. Make sure your code matches this exactly.
import requests
import sys
from config import VIRUSTOTAL_API_KEY, ABUSEIPDB_API_KEY
def check_virustotal(ip_address):
"""Query VirusTotal API v3 for IP address threat data."""
url = f"https://www.virustotal.com/api/v3/ip_addresses/{ip_address}"
headers = {
"x-apikey": VIRUSTOTAL_API_KEY
}
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
attributes = data["data"]["attributes"]
result = {
"malicious": attributes["last_analysis_stats"]["malicious"],
"suspicious": attributes["last_analysis_stats"]["suspicious"],
"harmless": attributes["last_analysis_stats"]["harmless"],
"undetected": attributes["last_analysis_stats"]["undetected"],
"country": attributes.get("country", "Unknown"),
"as_owner": attributes.get("as_owner", "Unknown"),
"reputation": attributes.get("reputation", 0),
}
return result
elif response.status_code == 401:
print("[VirusTotal] Error: Invalid API key.")
return None
elif response.status_code == 429:
print("[VirusTotal] Error: Rate limit exceeded. Try again later.")
return None
else:
print(f"[VirusTotal] Error: Unexpected status code {response.status_code}")
return None
except requests.exceptions.RequestException as e:
print(f"[VirusTotal] Network error: {e}")
return None
def check_abuseipdb(ip_address):
"""Query AbuseIPDB API v2 for IP abuse reports."""
url = "https://api.abuseipdb.com/api/v2/check"
headers = {
"Key": ABUSEIPDB_API_KEY,
"Accept": "application/json"
}
params = {
"ipAddress": ip_address,
"maxAgeInDays": 90
}
try:
response = requests.get(url, headers=headers, params=params)
if response.status_code == 200:
data = response.json()
report = data["data"]
result = {
"abuse_confidence_score": report["abuseConfidenceScore"],
"total_reports": report["totalReports"],
"country_code": report.get("countryCode", "Unknown"),
"isp": report.get("isp", "Unknown"),
"domain": report.get("domain", "Unknown"),
"usage_type": report.get("usageType", "Unknown"),
"is_tor": report.get("isTor", False),
}
return result
elif response.status_code == 401:
print("[AbuseIPDB] Error: Invalid API key.")
return None
elif response.status_code == 429:
print("[AbuseIPDB] Error: Rate limit exceeded. Try again later.")
return None
elif response.status_code == 422:
print("[AbuseIPDB] Error: Invalid IP address format.")
return None
else:
print(f"[AbuseIPDB] Error: Unexpected status code {response.status_code}")
return None
except requests.exceptions.RequestException as e:
print(f"[AbuseIPDB] Network error: {e}")
return None
def generate_report(ip_address):
"""Generate a consolidated threat intelligence report."""
print(f"\n{'='*55}")
print(f" THREAT INTELLIGENCE REPORT")
print(f" Target: {ip_address}")
print(f"{'='*55}\n")
# Query VirusTotal
print("[*] Querying VirusTotal...")
vt_result = check_virustotal(ip_address)
if vt_result:
print(f"\n--- VirusTotal Results ---")
print(f" Country: {vt_result['country']}")
print(f" Network Owner: {vt_result['as_owner']}")
print(f" Reputation: {vt_result['reputation']}")
print(f" Malicious: {vt_result['malicious']} engines")
print(f" Suspicious: {vt_result['suspicious']} engines")
print(f" Harmless: {vt_result['harmless']} engines")
print(f" Undetected: {vt_result['undetected']} engines")
else:
print(" VirusTotal lookup failed.")
# Query AbuseIPDB
print("\n[*] Querying AbuseIPDB...")
abuse_result = check_abuseipdb(ip_address)
if abuse_result:
print(f"\n--- AbuseIPDB Results ---")
print(f" Abuse Confidence Score: {abuse_result['abuse_confidence_score']}%")
print(f" Total Reports: {abuse_result['total_reports']}")
print(f" Country: {abuse_result['country_code']}")
print(f" ISP: {abuse_result['isp']}")
print(f" Domain: {abuse_result['domain']}")
print(f" Usage Type: {abuse_result['usage_type']}")
print(f" Tor Exit Node: {'Yes' if abuse_result['is_tor'] else 'No'}")
else:
print(" AbuseIPDB lookup failed.")
# Risk Assessment
print(f"\n{'='*55}")
print(f" RISK ASSESSMENT")
print(f"{'='*55}")
if vt_result and abuse_result:
is_dangerous = (
vt_result["malicious"] > 0
or abuse_result["abuse_confidence_score"] > 50
)
if is_dangerous:
print(" Status: POTENTIALLY DANGEROUS")
reasons = []
if vt_result["malicious"] > 0:
reasons.append(
f"{vt_result['malicious']} engines flagged as malicious"
)
if abuse_result["abuse_confidence_score"] > 50:
reasons.append(
f"Abuse confidence score: {abuse_result['abuse_confidence_score']}%"
)
for reason in reasons:
print(f" - {reason}")
else:
print(" Status: LIKELY CLEAN")
print(" No significant threats detected.")
else:
print(" Status: INCOMPLETE")
print(" Could not retrieve data from all sources.")
print(f"\n{'='*55}\n")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python3 threat_checker.py <ip_address>")
print("Example: python3 threat_checker.py 8.8.8.8")
sys.exit(1)
target_ip = sys.argv[1]
generate_report(target_ip)
Now let's test the tool with a known clean IP. Google's public DNS server (8.8.8.8) is a great test case because it should come back as safe.
- Make sure your virtual environment is still active (you should see (venv) at the start of your terminal prompt).
- Run your tool against Google's DNS by running this command:
python3 threat_checker.py 8.8.8.8
You should see a full threat intelligence report in your terminal. It starts with a header banner, followed by VirusTotal results (country, network owner, engine counts), AbuseIPDB results (abuse score, ISP, usage type), and a RISK ASSESSMENT section showing LIKELY CLEAN.
Seeing an error instead of a report?
- If you see [VirusTotal] Error: Invalid API key or [AbuseIPDB] Error: Invalid API key, double check that your keys in config.py are correct and don't have extra spaces or quotes around them.
- If you see ModuleNotFoundError: No module named 'config', make sure config.py is in the same folder as threat_checker.py and that you're running the command from the threat-checker directory.
- If you see IndentationError, the risk assessment code may not be aligned with the rest of generate_report. Use the "double check the full code" tab above to compare your file.
Help me debug my threat checker script
Now try your tool on a different IP to see how the results compare. You can search for recently reported IPs on the AbuseIPDB website's "Most Reported" page to find a test target.
Finding a malicious test IP
Visit abuseipdb.com and browse the recently reported IPs on the homepage. Pick one with a high confidence score and run your tool against it. You should see a POTENTIALLY DANGEROUS status with specific reasons listed.
Secret mission
Add Domain Lookups
Your threat checker investigates IP addresses. But what about suspicious domains? Extend your tool to look up domain names using the VirusTotal domain endpoint, so you can investigate both IPs and domains from a single command.
Before you go
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- threat-checker project directory (includes virtual environment, threat_checker.py, and config.py).
- Python 3.14 installation.
- VirusTotal account (free tier).
- AbuseIPDB account (free tier).
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your threat checker or want to extend it further.
- Your project files, virtual environment, and API accounts stay as they are.
- Both VirusTotal and AbuseIPDB free tiers have no monthly charges, so your accounts cost nothing to keep.
- Your API keys in config.py remain active and ready to use.
Pause - I'll come back to this later
Deactivate your virtual environment to free up your terminal session. Your files and accounts stay intact for when you return.
- In your terminal, run deactivate to exit the virtual environment.
- When you come back, navigate to your threat-checker folder and run source venv/bin/activate to reactivate the virtual environment.
- Your VirusTotal and AbuseIPDB accounts will stay active with no action needed.
Delete - I don't want to use this again
Remove all project files and optionally delete your API accounts to start completely fresh.
- Deactivate the virtual environment by running deactivate in your terminal.
- Delete the entire threat-checker folder from your Desktop. This removes the virtual environment, your scripts, and the config.py file containing your API keys.
- To delete your VirusTotal account, go to your profile settings on virustotal.com and look for the account deletion option.
- To delete your AbuseIPDB account, go to your account settings on abuseipdb.com and look for the account deletion option.
- Python itself can stay installed on your machine. It is useful for future projects.
Mission accomplished
That's a Wrap!
Nice work! You just built a real threat intelligence tool that cloud security engineers use every day to investigate suspicious activity.
You've learned how to:
- Built a Python CLI tool that queries two real-world security APIs (VirusTotal and AbuseIPDB) using authenticated HTTP requests with custom headers.
- Parsed JSON responses to extract meaningful security data and implemented error handling for common API issues like invalid keys, rate limits, and network errors.
- Created a consolidated threat report that combines data from multiple sources into a single risk assessment displayed in the terminal.
- Secret Mission: Extended the tool to investigate both IP addresses and domain names using the VirusTotal domain endpoint.
Ready to quiz yourself?