diff --git a/.gitignore b/.gitignore
index c26136f..601e383 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,6 +57,7 @@ venv
 
 ## Data
 data/*
+*.json
 
 # swap files
 *.swp
diff --git a/utilitybelt/utilitybelt.py b/utilitybelt/utilitybelt.py
index 180865c..8307e7a 100644
--- a/utilitybelt/utilitybelt.py
+++ b/utilitybelt/utilitybelt.py
@@ -143,6 +143,24 @@ def is_url(url):
     return bool(re.match(re_url, url))
 
 
+def is_hash(fhash):
+    """Returns true for valid hashes, false for invalid."""
+
+    # Intentionally doing if/else statement for ease of testing and reading
+    if re.match(re_md5, fhash):
+        return True
+    elif re.match(re_sha1, fhash):
+        return True
+    elif re.match(re_sha256, fhash):
+        return True
+    elif re.match(re_sha512, fhash):
+        return True
+    elif re.match(re_ssdeep, fhash):
+        return True
+    else:
+        return False
+
+
 def ip_to_geo(ipaddress):
     """Convert IP to Geographic Information"""
 
@@ -245,7 +263,10 @@ def vt_ip_check(ip, vt_api):
     url = 'https://www.virustotal.com/vtapi/v2/ip-address/report'
     parameters = {'ip': ip, 'apikey': vt_api}
     response = requests.get(url, params=parameters)
-    return response.json()
+    try:
+        return response.json()
+    except ValueError:
+        return None
 
 
 def vt_name_check(domain, vt_api):
@@ -256,7 +277,24 @@ def vt_name_check(domain, vt_api):
     url = 'https://www.virustotal.com/vtapi/v2/domain/report'
     parameters = {'domain': domain, 'apikey': vt_api}
     response = requests.get(url, params=parameters)
-    return response.json()
+    try:
+        return response.json()
+    except ValueError:
+        return None
+
+
+def vt_hash_check(fhash, vt_api):
+    """Checks VirusTotal for occurrences of a file hash"""
+    if not is_hash(fhash):
+        return None
+
+    url = 'https://www.virustotal.com/vtapi/v2/file/report'
+    parameters = {'resource': fhash, 'apikey': vt_api}
+    response = requests.get(url, params=parameters)
+    try:
+        return response.json()
+    except ValueError:
+        return None
 
 
 def ipinfo_ip_check(ip):

`vt_name_check()` and `vt_api_check()` do not handle errors properly
If VirusTotal returns an error (such as exceeding rate limits), these functions throw a `JSONDecodeError`.
diff --git a/utilitybelt/tests/tests.py b/utilitybelt/tests/tests.py
index b826966..ea1f525 100644
--- a/utilitybelt/tests/tests.py
+++ b/utilitybelt/tests/tests.py
@@ -1,4 +1,5 @@
 import os
+import time
 import unittest
 
 from utilitybelt import utilitybelt as ub
@@ -94,6 +95,21 @@ class TestUB(unittest.TestCase):
         self.assertFalse(ub.is_reserved("8.8.4.4"))
         self.assertFalse(ub.is_reserved("192.30.252.131"))
 
+    def test_is_hash(self):
+        # all hashes of the empty string
+        self.assertIsInstance(ub.is_hash("d41d8cd98f00b204e9800998ecf8427e"), bool)
+        # MD5
+        self.assertTrue(ub.is_hash("d41d8cd98f00b204e9800998ecf8427e"))
+        # SHA1
+        self.assertTrue(ub.is_hash("da39a3ee5e6b4b0d3255bfef95601890afd80709"))
+        # SHA256
+        self.assertTrue(ub.is_hash("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"))
+        # SHA512
+        self.assertTrue(ub.is_hash("cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"))
+        # ssdeep
+        self.assertTrue(ub.is_hash("96:EQOJvOl4ab3hhiNFXc4wwcweomr0cNJDBoqXjmAHKX8dEt001nfEhVIuX0dDcs:3mzpAsZpprbshfu3oujjdENdp21"))
+        self.assertFalse(ub.is_hash("KilroyWasHere"))
+
     def test_vt_ip_check(self):
         vt_api = os.environ["VT_API"]
         self.assertIsNone(ub.vt_ip_check('asdf', vt_api))
@@ -108,6 +124,7 @@ class TestUB(unittest.TestCase):
             if resolution['hostname'] == "github.com":
                 is_gh = True
         self.assertTrue(is_gh)
+        time.sleep(15)  # VT rate limiting
 
     def test_vt_name_check(self):
         vt_api = os.environ["VT_API"]
@@ -120,6 +137,37 @@ class TestUB(unittest.TestCase):
             if resolution['ip_address'] == '192.30.252.130':
                 is_gh = True
         self.assertTrue(is_gh)
+        time.sleep(15)  # VT rate limiting
+
+    def test_vt_hash_check(self):
+        vt_api = os.environ["VT_API"]
+        self.assertIsNone(ub.vt_hash_check('asdf', vt_api))
+        vt_hash_data = ub.vt_hash_check("fe03b4181707f1ea1f3c69dc0a9904181c6fce91", vt_api)
+        self.assertIsInstance(vt_hash_data, dict)
+        self.assertIn('resource', vt_hash_data)
+        self.assertIn('positives', vt_hash_data)
+        self.assertGreater(vt_hash_data['positives'], 0)
+        time.sleep(15)  # VT rate limiting
+        vt_hash_data = ub.vt_hash_check("d41d8cd98f00b204e9800998ecf8427e", vt_api)
+        self.assertIn('positives', vt_hash_data)
+        self.assertEqual(vt_hash_data['positives'], 0)
+        time.sleep(15)  # VT rate limiting
+
+    def test_vt_rate_limiting(self):
+        vt_api = os.environ["VT_API"]
+        # Exceed 4x in 60 seconds
+        data = ub.vt_hash_check("d41d8cd98f00b204e9800998ecf8427e", vt_api)
+        self.assertIsInstance(data, dict)
+        data = ub.vt_hash_check("d41d8cd98f00b204e9800998ecf8427e", vt_api)
+        data = ub.vt_hash_check("d41d8cd98f00b204e9800998ecf8427e", vt_api)
+        data = ub.vt_hash_check("d41d8cd98f00b204e9800998ecf8427e", vt_api)
+        data = ub.vt_name_check("example.org", vt_api)
+        self.assertIsNone(data)
+        data = ub.vt_ip_check("192.30.252.130", vt_api)
+        self.assertIsNone(data)
+        data = ub.vt_hash_check("d41d8cd98f00b204e9800998ecf8427e", vt_api)
+        self.assertIsNone(data)
+        time.sleep(15)
 
     def test_ipinfo(self):
         self.assertIsNone(ub.ipinfo_ip_check('asdf'))

pip install -e .
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc libgeoip-dev
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
beautifulsoup4==4.13.3
certifi==2025.1.31
cfgv==3.4.0
charset-normalizer==3.4.1
-e git+https://github.com/yolothreat/utilitybelt.git@ef2526c4e15c886c2a68162b67e7d7fb25ec9ee3#egg=cnd_utilitybelt
coverage==7.8.0
coveralls==4.0.1
distlib==0.3.9
docopt==0.6.2
exceptiongroup==1.2.2
filelock==3.18.0
identify==2.6.9
idna==3.10
iniconfig==2.1.0
netaddr==1.3.0
nodeenv==1.9.1
packaging==24.2
platformdirs==4.3.7
pluggy==1.5.0
pre_commit==4.2.0
pygeoip==0.3.2
pytest==8.3.5
pytest-cov==6.0.0
PyYAML==6.0.2
requests==2.32.3
soupsieve==2.6
tomli==2.2.1
typing_extensions==4.13.0
urllib3==2.3.0
virtualenv==20.29.3

name: utilitybelt
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - beautifulsoup4==4.13.3
      - certifi==2025.1.31
      - cfgv==3.4.0
      - charset-normalizer==3.4.1
      - coverage==7.8.0
      - coveralls==4.0.1
      - distlib==0.3.9
      - docopt==0.6.2
      - exceptiongroup==1.2.2
      - filelock==3.18.0
      - identify==2.6.9
      - idna==3.10
      - iniconfig==2.1.0
      - netaddr==1.3.0
      - nodeenv==1.9.1
      - packaging==24.2
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pre-commit==4.2.0
      - pygeoip==0.3.2
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pyyaml==6.0.2
      - requests==2.32.3
      - soupsieve==2.6
      - tomli==2.2.1
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
prefix: /opt/conda/envs/utilitybelt

utilitybelt/tests/tests.py::TestUB::test_is_hash
utilitybelt/tests/tests.py::TestUB::test_domain_to_geo utilitybelt/tests/tests.py::TestUB::test_dshield_ip_check utilitybelt/tests/tests.py::TestUB::test_ipinfo utilitybelt/tests/tests.py::TestUB::test_ipvoid_check utilitybelt/tests/tests.py::TestUB::test_reverse_dns utilitybelt/tests/tests.py::TestUB::test_reverse_dns_sna utilitybelt/tests/tests.py::TestUB::test_urlvoid_check utilitybelt/tests/tests.py::TestUB::test_urlvoid_ip_check utilitybelt/tests/tests.py::TestUB::test_vt_hash_check utilitybelt/tests/tests.py::TestUB::test_vt_ip_check utilitybelt/tests/tests.py::TestUB::test_vt_name_check utilitybelt/tests/tests.py::TestUB::test_vt_rate_limiting
utilitybelt/tests/tests.py::TestUB::test_ip_between utilitybelt/tests/tests.py::TestUB::test_ip_to_geo utilitybelt/tests/tests.py::TestUB::test_ip_to_geojson utilitybelt/tests/tests.py::TestUB::test_ip_to_long utilitybelt/tests/tests.py::TestUB::test_ips_to_geojson utilitybelt/tests/tests.py::TestUB::test_is_IPv4Address utilitybelt/tests/tests.py::TestUB::test_is_reserved utilitybelt/tests/tests.py::TestUB::test_is_rfc1918 utilitybelt/tests/tests.py::TestUB::test_is_url
.gitignore utilitybelt/utilitybelt.py
.gitignore utilitybelt/utilitybelt.py