Tools · Python and Bash for security automation · 05 / 20
Enriching file hashes with the VirusTotal API in Python, safely and within rate limits
Look up hashes, not files, with the VirusTotal v3 API, respect free-tier rate limits, cache results, and summarize detections for triage.
Hashes, not uploads
Looking up a hash asks “has anyone seen this file before?” without sharing the file. Uploading a file shares its contents with the service and its subscribers, which can leak internal documents or tip off an attacker that you found their tool. In triage, start with hash lookups and upload only with approval.
The API
The v3 endpoint GET /api/v3/files/{hash} returns metadata including last_analysis_stats with counts of malicious, suspicious, undetected, and harmless verdicts. Authenticate with the x-apikey header. The free public API has tight rate limits (a few requests per minute and a daily quota), so add sleep and caching. A 404 means the hash is unknown, which is not the same as safe.
#!/usr/bin/env python3
"""Look up SHA-256 hashes on VirusTotal with caching and rate limiting."""
import json, os, sys, time, requests
KEY = os.environ['VT_API_KEY']
CACHE = 'vt_cache.json'
cache = json.load(open(CACHE)) if os.path.exists(CACHE) else {}
def lookup(h):
if h in cache:
return cache[h]
r = requests.get(f'https://www.virustotal.com/api/v3/files/{h}', headers={'x-apikey': KEY}, timeout=20)
if r.status_code == 404:
result = {'status': 'unknown'}
elif r.status_code == 429:
time.sleep(60); return lookup(h)
else:
r.raise_for_status()
a = r.json()['data']['attributes']
result = {'status': 'known', 'stats': a['last_analysis_stats'], 'name': a.get('meaningful_name'), 'first_seen': a.get('first_submission_date')}
cache[h] = result
json.dump(cache, open(CACHE, 'w'))
time.sleep(15) # stay under the public API rate limit
return result
for line in sys.stdin:
h = line.strip().lower()
if len(h) == 64:
res = lookup(h)
stats = res.get('stats', {})
print(h, res['status'], f"malicious={stats.get('malicious', '-')}", res.get('name') or '')Lab: enrich a hash list
Create a free VirusTotal account, put the API key in an environment variable, and run the script on a list containing the SHA-256 of the EICAR test file and the hash of a common system binary such as /bin/ls from your own machine. The EICAR hash shows many detections; your ls hash may be known and clean or unknown. Discuss why “unknown” does not mean “safe”.
export VT_API_KEY=... # from your account settings (curl -s https://secure.eicar.org/eicar.com.txt | sha256sum | cut -d' ' -f1; sha256sum /bin/ls | cut -d' ' -f1) | python3 vt.py
Interpreting results
A handful of detections from lesser-known engines can be false positives; many detections from major engines is strong evidence. Unknown hashes are common for custom or newly compiled files, including targeted malware. Combine results with context from your own telemetry: where the file came from, what it did, and how many hosts have it.
Diagram
Lookup outcomes
A result is context, not a verdict.
- Known, many malicious
Strong evidence; investigate and contain.
- Known, few detections
Possible false positive; check context.
- Known, clean
Common legitimate file, but still check behavior.
- Unknown (404)
Never seen; custom, new, or targeted. Not proof of safety.
Check yourself
1. Why look up a hash instead of uploading the file?
2. The API returns 404 for a hash. What does that mean?
3. How does the script stay within free-tier limits?