diff --git a/src/ioc_scan/ioc_scanner.py b/src/ioc_scan/ioc_scanner.py
index 5d98b02..94cd952 100755
--- a/src/ioc_scan/ioc_scanner.py
+++ b/src/ioc_scan/ioc_scanner.py
@@ -11,11 +11,10 @@ This script should be run as a priveledged user.
 
 from collections import defaultdict
 from datetime import datetime
+import hashlib
 import logging
-import platform
+import os
 import re
-import subprocess  # nosec
-from string import Template
 import sys
 
 # just paste the text that has the indicators into BLOB.
@@ -35,6 +34,11 @@ MDS: e803916dd56996d7dald4013d71d05dd
 MDS: 2a2410cef5497cbd3f6c13eaff9619da
 MDS: 3e7eb6abcce304de0822a618de756fd2
 MDS: 350cba65e28c723cbf0724c19bd7ee69
+SHA256: b509f8545501588ecd828f970d91afc7c4aa6e238e838bd6a08ee2cd920fbe98
+SHA-1:  31B54AEBDAF5FBC73A66AC41CCB35943CC9B7F72
+SHA-1:  50973A3FC57D70C7911F7A952356188B9939E56B
+SHA-1:  244EB62B9AC30934098CA4204447440D6FC4E259
+SHA-1:  5C8F83CC4FF57E7C67925DF4D9DAABE5D0CC07E2
 few things that should hit:
 GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)
 0313fd399b143fc40cd52a1679018305
@@ -44,11 +48,30 @@ EICAR test file
 69630e4574ec6798239b091cda43dca0
 """
 
-MD5_RE = r"([a-fA-F\d]{32})"
-COMMANDS = {
-    "Linux": Template(r"find $root -xdev -type f -exec md5sum {} \;"),
-    "Darwin": Template(r"find $root -xdev -type f -exec md5 -r {} \;"),
-}
+# use word boundaries ('\b') to bracket the specific hash lengths
+MD5_RE = r"\b([a-fA-F\d]{32})\b"
+SHA1_RE = r"\b([a-fA-F\d]{40})\b"
+SHA256_RE = r"\b([a-fA-F\d]{64})\b"
+
+
+def hash_file(file):
+    """Generate MD5, SHA1, and SHA256 hashes for a given file."""
+    hash_md5 = hashlib.md5()  # nosec
+    hash_sha1 = hashlib.sha1()  # nosec
+    hash_sha256 = hashlib.sha256()
+
+    # try except to eat filesystem errors like Permission Denied etc
+    try:
+        with open(file, "rb") as f:
+            # read it in chunks so memory use isn't outlandish
+            for chunk in iter(lambda: f.read(4096), b""):
+                hash_md5.update(chunk)
+                hash_sha1.update(chunk)
+                hash_sha256.update(chunk)
+    except OSError:
+        pass
+
+    return (hash_md5.hexdigest(), hash_sha1.hexdigest(), hash_sha256.hexdigest())
 
 
 def main(blob=None, root="/"):
@@ -60,39 +83,48 @@ def main(blob=None, root="/"):
         blob = BLOB
 
     # get a list of all the md5 hashes from some inconsiderate source.
-    indicators = re.findall(MD5_RE, blob.lower())
+    indicators_md5 = re.findall(MD5_RE, blob.lower())
+    indicators_sha1 = re.findall(SHA1_RE, blob.lower())
+    indicators_sha256 = re.findall(SHA256_RE, blob.lower())
+    indicators = indicators_md5 + indicators_sha1 + indicators_sha256
+
     logging.debug(f"Scan will search for {len(indicators)} indicators")
 
     # compile a regular expression to search for all indicators
     indicators_re = re.compile("|".join(indicators))
 
-    # choose the correct command based on the platform, and apply root to template
-    command = COMMANDS.get(platform.system()).substitute(root=root)
-    logging.debug(f"Scan command: {command}")
-
     # start hashing files
     logging.debug(f"Starting scan with root: {root}")
-    p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)  # nosec
-    logging.debug("Scan completed")
 
+    # store an array of ioc hits
+    ioc_list = []
     # keep a tally of the hits
     tallies = defaultdict(lambda: 0)
+    # walk the filesystem starting at root
+    for rootdir, subdirs, files in os.walk(root):
+        # find -xdev equivalent
+        subdirs[:] = [
+            d for d in subdirs if not os.path.ismount(os.path.join(rootdir, d))
+        ]
+
+        # check each file in the current directory
+        for file in [os.path.join(rootdir, f) for f in files]:
+            # get hashes for the current file
+            hashes = hash_file(file)
+
+            for hash in hashes:
+                matches = indicators_re.findall(hash)
+
+                # tally it up and report if we get a hit
+                if matches:
+                    ioc_list.append(f"{hash} {file}")
+                    tallies[hash] += 1
 
-    for line in p.stdout:
-        line = line.decode("utf-8")
-        # a line looks like this:
-        # 0313fd399b143fc40cd52a1679018305  /bin/bash
-
-        # save just the hash
-        file_hash = line.split()[0]
-
-        # check the line for matches
-        matches = indicators_re.findall(file_hash)
+    logging.debug("Scan completed")
 
-        # tally it up and report if we get a hit
-        if matches:
-            print(line)
-            tallies[matches[0]] += 1
+    # print all indicators that were found
+    for ioc in ioc_list:
+        print(ioc)
 
     # stop the clock
     end_time = datetime.utcnow()

Support hashes besides MD5
# 🚀 Feature Proposal

Add the ability to scan for other hash types besides MD5, such as SHA-1 and SHA-256.

## Motivation

When we are asked to scan for Indicators of Compromise (IOCs), we occasionally are given SHA-1 and SHA-256 hashes, in addition to MD5 hashes.

## Example

Sample IOC hashes:
```
SHA256: b509f8545501588ecd828f970d91afc7c4aa6e238e838bd6a08ee2cd920fbe98
SHA-1:  31B54AEBDAF5FBC73A66AC41CCB35943CC9B7F72
SHA-1:  50973A3FC57D70C7911F7A952356188B9939E56B
SHA-1:  244EB62B9AC30934098CA4204447440D6FC4E259
SHA-1:  5C8F83CC4FF57E7C67925DF4D9DAABE5D0CC07E2
```

## Pitch

It will give us more comprehensive scanning capabilities.

diff --git a/tests/test_ioc_scan.py b/tests/test_ioc_scan.py
index 6ab4b37..33437e3 100644
--- a/tests/test_ioc_scan.py
+++ b/tests/test_ioc_scan.py
@@ -10,6 +10,7 @@ import pytest
 
 import ioc_scan
 from ioc_scan import ioc_scan_cli
+from ioc_scan import ioc_scanner
 
 
 log_levels = (
@@ -50,6 +51,27 @@ def test_log_levels(level):
             assert return_code == 0, "main() should return success (0)"
 
 
+def test_hash_file_hashing():
+    """Test that hashes are being generated correctly."""
+    hashes = ioc_scanner.hash_file("tests/targets/eicar.txt")
+    assert hashes[0] == "69630e4574ec6798239b091cda43dca0"
+    assert hashes[1] == "cf8bd9dfddff007f75adf4c2be48005cea317c62"
+    assert (
+        hashes[2] == "131f95c51cc819465fa1797f6ccacf9d494aaaff46fa3eac73ae63ffbdfd8267"
+    )
+
+
+def test_hash_file_except():
+    """Test that hash_file() passes when an OSError exception is raised."""
+    hashes = ioc_scanner.hash_file("tests/targets/doesnotexist.txt")
+    # values for hashes of nothing
+    assert hashes[0] == "d41d8cd98f00b204e9800998ecf8427e"
+    assert hashes[1] == "da39a3ee5e6b4b0d3255bfef95601890afd80709"
+    assert (
+        hashes[2] == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+    )
+
+
 def test_scan_file(capsys):
     """Test running the scanner with an input target file."""
     with patch.object(

pip install -e .[test]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
cfgv==3.3.1
charset-normalizer==3.4.1
coverage==6.5.0
coveralls==3.3.1
distlib==0.3.9
docopt==0.6.2
exceptiongroup==1.2.2
filelock==3.12.2
identify==2.5.24
idna==3.10
importlib-metadata==6.7.0
iniconfig==2.0.0
-e git+https://github.com/cisagov/ioc-scanner.git@e6bfb28c3ed13c4c9df9b93854fb3e6bd0454bf0#egg=ioc_scan
nodeenv==1.9.1
packaging==24.0
platformdirs==4.0.0
pluggy==1.2.0
pre-commit==2.21.0
pytest==7.4.4
pytest-cov==4.1.0
PyYAML==6.0.1
requests==2.31.0
tomli==2.0.1
typing_extensions==4.7.1
urllib3==2.0.7
virtualenv==20.26.6
zipp==3.15.0

name: ioc-scanner
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
  - certifi=2022.12.7=py37h06a4308_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=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - cfgv==3.3.1
      - charset-normalizer==3.4.1
      - coverage==6.5.0
      - coveralls==3.3.1
      - distlib==0.3.9
      - docopt==0.6.2
      - exceptiongroup==1.2.2
      - filelock==3.12.2
      - identify==2.5.24
      - idna==3.10
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - nodeenv==1.9.1
      - packaging==24.0
      - platformdirs==4.0.0
      - pluggy==1.2.0
      - pre-commit==2.21.0
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - pyyaml==6.0.1
      - requests==2.31.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - virtualenv==20.26.6
      - zipp==3.15.0
prefix: /opt/conda/envs/ioc-scanner

tests/test_ioc_scan.py::test_hash_file_hashing tests/test_ioc_scan.py::test_hash_file_except
tests/test_ioc_scan.py::test_version tests/test_ioc_scan.py::test_log_levels[debug] tests/test_ioc_scan.py::test_log_levels[info] tests/test_ioc_scan.py::test_log_levels[warning] tests/test_ioc_scan.py::test_log_levels[error] tests/test_ioc_scan.py::test_log_levels[critical] tests/test_ioc_scan.py::test_scan_file tests/test_ioc_scan.py::test_scan_stdin
src/ioc_scan/ioc_scanner.py
src/ioc_scan/ioc_scanner.py