How I created a successful recon tool for bug hunters and how you can build one too



This content originally appeared on DEV Community and was authored by Marília Rocha

Recon is one of the most important steps when you’re looking for vulnerabilities. I use several tools in my workflow to speed things up, and one of the ones I created myself is called malwaricon (GitHub link). I based it on a script originally written at MIT and adapted it to fit how I work during bug bounty programs.

malwaricon helped me spot new assets, map out endpoints quickly, and keep my recon work organized-all with a single script. It’s been super useful, and if it worked for me, you can definitely create something even better for yourself.

In this post, I’m going to walk you through exactly how I built this script, step by step. No magic, no black box, just plain Bash scripting and some great open-source tools. By the end, you’ll have your own recon framework to use and expand.
Why write your own recon script?

Yes, there are a ton of great tools out there like Amass, Subfinder, Assetfinder, and more. Each of them is good at one thing. But if you want to automate your workflow and stop running the same commands over and over again, writing your own recon script can make a huge difference.

Here’s what you gain:

You control everything: You choose which tools to run, how they run, and where the output goes.
It saves time: No more typing long commands by hand. Just run a script and let it handle the rest.
It’s flexible: Add or remove tools whenever you want. Tweak it as your methodology evolves.
You get organized results: Save output in structured folders, generate reports, and track changes over time.
You can automate everything: Schedule scans with cron, track differences with Git, and get alerts when something changes.
You learn a lot: Building the script forces you to understand how tools like Nmap, Dirsearch, and crt.sh actually work.

This isn’t just for beginners either. Even experienced bug hunters use scripts like this to streamline their workflow. With something like malwaricon, recon becomes a fast, repeatable, and reliable part of your routine.
Understanding bash scripting basics

Let’s start by writing a simple Bash script. Open any text editor. Every script should begin with a shebang line to declare the interpreter:

#!/bin/bash

This tells the system to use Bash to run the script. Let’s say we want to run Nmap and Dirsearch against a target:

#!/bin/bash
nmap scanme.nmap.org
/PATH/TO/dirsearch.py -u scanme.nmap.org -e php

Not very flexible yet. Let’s modify it to accept a target domain from the user:

#!/bin/bash
nmap $1
/PATH/TO/dirsearch.py -u $1 -e php

Here, $1 is the first argument passed to the script (your domain). If you run:

./recon.sh scanme.nmap.org

It will scan that domain.

But what if dirsearch.py isn’t in the same folder? You’ll need the full path:

/PATH/TO/dirsearch.py

Or you can add the folder to your system’s PATH so it can be called from anywhere:

export PATH="/your/dirsearch/path:$PATH"

To make that change permanent, add it to your ~/.bash_profile.

Now update the script like this:
`

!/bin/bash

nmap $1
dirsearch.py -u $1 -e php`

Save the file as recon.sh and make it executable:

chmod +x recon.sh

Now you can run it like this:

./recon.sh scanme.nmap.org

If you get a permission error, it’s likely the file isn’t executable yet. The chmod command fixes that.
Saving tool output to files

To make your recon work easier to review later, you’ll want to save the output into organized folders. Let’s improve the script by creating a directory for each scan and redirecting the output of the tools into specific files:

#!/bin/bash
echo "Creating directory $1_recon."
mkdir $1_recon
nmap $1 > $1_recon/nmap
echo "The results of nmap scan are stored in $1_recon/nmap."
/PATH/TO/dirsearch.py -u $1 -e php --simple-report=$1_recon/dirsearch
echo "The results of dirsearch scan are stored in $1_recon/dirsearch."

This version:

Creates a folder named domain_recon.
Stores the Nmap result in nmap.
Stores the Dirsearch result in dirsearch.

We can make this even cleaner by using variables:

#!/bin/bash
PATH_TO_DIRSEARCH="/Users/yourname/tools/dirsearch"
DOMAIN=$1
DIRECTORY=${DOMAIN}_recon
echo "Creating directory $DIRECTORY."
mkdir $DIRECTORY
nmap $DOMAIN > $DIRECTORY/nmap
echo "The results of nmap scan are stored in $DIRECTORY/nmap."
$PATH_TO_DIRSEARCH/dirsearch.py -u $DOMAIN -e php --simple-report=$DIRECTORY/dirsearch
echo "The results of dirsearch scan are stored in $DIRECTORY/dirsearch."

This small change will help you avoid hardcoding values and make your script easier to update later if needed.
Adding the date of the scan to the output

Sometimes you want to know exactly when a scan was performed. You can add the current date to your script’s output by using command substitution in Bash. Here’s how:

#!/bin/bash
PATH_TO_DIRSEARCH="/Users/yourname/tools/dirsearch"
TODAY=$(date)
echo "This scan was created on $TODAY"
DOMAIN=$1
DIRECTORY=${DOMAIN}_recon
echo "Creating directory $DIRECTORY."
mkdir $DIRECTORY
nmap $DOMAIN > $DIRECTORY/nmap
echo "The results of nmap scan are stored in $DIRECTORY/nmap."
$PATH_TO_DIRSEARCH/dirsearch.py -u $DOMAIN -e php --simple-report=$DIRECTORY/dirsearch
echo "The results of dirsearch scan are stored in $DIRECTORY/dirsearch."

Here, the command $(date) runs the system date command and stores the output in the variable TODAY. Then you print that out as a message.
Adding options to choose the tools to run

Maybe you want to choose whether to run only Nmap, only Dirsearch, or both. You can add a second argument to specify the scan mode and then use conditional statements to decide what to run.

Here’s an example using if-else:

#!/bin/bash
PATH_TO_DIRSEARCH="/Users/yourname/tools/dirsearch"
TODAY=$(date)
echo "This scan was created on $TODAY"
DOMAIN=$1
DIRECTORY=${DOMAIN}_recon
echo "Creating directory $DIRECTORY."
mkdir $DIRECTORY 

if [ "$2" == "nmap-only" ]; then
  nmap $DOMAIN > $DIRECTORY/nmap
  echo "The results of nmap scan are stored in $DIRECTORY/nmap."
elif [ "$2" == "dirsearch-only" ]; then  
  $PATH_TO_DIRSEARCH/dirsearch.py -u $DOMAIN -e php --simple-report=$DIRECTORY/dirsearch
  echo "The results of dirsearch scan are stored in $DIRECTORY/dirsearch."
else
  nmap $DOMAIN > $DIRECTORY/nmap
  echo "The results of nmap scan are stored in $DIRECTORY/nmap."
  $PATH_TO_DIRSEARCH/dirsearch.py -u $DOMAIN -e php --simple-report=$DIRECTORY/dirsearch
  echo "The results of dirsearch scan are stored in $DIRECTORY/dirsearch."
fi

Using case statements to simplify multiple options

If you want to add more options (like a third tool crt.sh), using if-else can get messy. Instead, you can use a case statement for cleaner code:

#!/bin/bash
PATH_TO_DIRSEARCH="/Users/yourname/tools/dirsearch"
TODAY=$(date)
echo "This scan was created on $TODAY"
DOMAIN=$1
DIRECTORY=${DOMAIN}_recon
echo "Creating directory $DIRECTORY."
mkdir $DIRECTORY
case $2 in
  nmap-only)
    nmap $DOMAIN > $DIRECTORY/nmap
    echo "The results of nmap scan are stored in $DIRECTORY/nmap."
    ;;
  dirsearch-only)
    $PATH_TO_DIRSEARCH/dirsearch.py -u $DOMAIN -e php --simple-report=$DIRECTORY/dirsearch
    echo "The results of dirsearch scan are stored in $DIRECTORY/dirsearch."
    ;;         
  crt-only)       
    curl "https://crt.sh/?q=$DOMAIN&output=json" -o $DIRECTORY/crt
    echo "The results of cert parsing is stored in $DIRECTORY/crt."
    ;;
  *)
    nmap $DOMAIN > $DIRECTORY/nmap
    echo "The results of nmap scan are stored in $DIRECTORY/nmap."
    $PATH_TO_DIRSEARCH/dirsearch.py -u $DOMAIN -e php --simple-report=$DIRECTORY/dirsearch
    echo "The results of dirsearch scan are stored in $DIRECTORY/dirsearch."
    curl "https://crt.sh/?q=$DOMAIN&output=json" -o $DIRECTORY/crt
    echo "The results of cert parsing is stored in $DIRECTORY/crt."
    ;;
esac

Creating functions to avoid code repetition

To keep the script tidy, define functions for each scan:

#!/bin/bash
PATH_TO_DIRSEARCH="/Users/yourname/tools/dirsearch"
TODAY=$(date)
echo "This scan was created on $TODAY"
DOMAIN=$1
DIRECTORY=${DOMAIN}_recon
echo "Creating directory $DIRECTORY."
mkdir $DIRECTORY

nmap_scan() {
  nmap $DOMAIN > $DIRECTORY/nmap
  echo "The results of nmap scan are stored in $DIRECTORY/nmap."
}

dirsearch_scan() {
  $PATH_TO_DIRSEARCH/dirsearch.py -u $DOMAIN -e php --simple-report=$DIRECTORY/dirsearch
  echo "The results of dirsearch scan are stored in $DIRECTORY/dirsearch."
}

crt_scan() {
  curl "https://crt.sh/?q=$DOMAIN&output=json" -o $DIRECTORY/crt
  echo "The results of cert parsing is stored in $DIRECTORY/crt."
}

case $2 in
  nmap-only)
    nmap_scan
    ;;
  dirsearch-only)
    dirsearch_scan
    ;;         
  crt-only)       
    crt_scan
    ;;
  *)
    nmap_scan
    dirsearch_scan
    crt_scan
    ;;     
esac

Parsing the results with grep and jq

After your scans finish, you’ll have output files packed with information. Reading all that manually can be tedious and inefficient. To speed things up, you can use grep to search for specific patterns in files and jq to parse JSON output, like from crt.sh.
Using grep to filter Nmap output

For example, Nmap’s default output contains lots of status lines, but you might only want the lines showing open ports. Those typically have three columns: port, state, and service.

grep -E "^\s*\S+\s+\S+\s+\S+\s*$" $DIRECTORY/nmap > $DIRECTORY/nmap_cleaned

Explanation:

The -E option enables extended regular expressions
The regex matches lines with exactly three non-whitespace strings separated by spaces, filtering out unnecessary lines.

Running this command produces nmap_cleaned, a clean list of ports and their states.
Using jq to extract data from crt.sh JSON

crt.sh returns JSON containing many fields. To extract all domain names from certificates, use:

jq -r ".[] | .name_value" $DIRECTORY/crt

Explanation:

-r outputs raw strings (no quotes).
.[] iterates over each item in the JSON array.
.name_value pulls out the domain names

Building a master report

To combine everything into a single report file, you can aggregate the outputs like this:

echo “Generating recon report…”

TODAY=$(date)
echo "This scan was created on $TODAY" > $DIRECTORY/report

echo "Results for Nmap:" >> $DIRECTORY/report
grep -E "^\s*\S+\s+\S+\s+\S+\s*$" $DIRECTORY/nmap >> $DIRECTORY/report

echo "Results for Dirsearch:" >> $DIRECTORY/report
cat $DIRECTORY/dirsearch >> $DIRECTORY/report

echo "Results for crt.sh:" >> $DIRECTORY/report
jq -r ".[] | .name_value" $DIRECTORY/crt >> $DIRECTORY/report

This report file will be nicely organized, containing everything you need to analyze your target in one place.
Scanning multiple domains with getopts and loops

Sometimes you want to scan several domains at once, for example:

./recon.sh -m nmap-only example.com test.com

Here, -m specifies the scan mode (like nmap-only), and the rest are domains.
Parsing options with getopts

getopts helps you handle flags and their values. Here’s how to parse the -m option for mode:

`getopts “m:” OPTION
MODE=$OPTARG

"m:" means -m expects an argument.
$OPTARG stores the argument’s value (e.g., nmap-only).

`
Looping through domains

After parsing options, $OPTIND tells you the index of the first non-option argument (the domains). You can loop through all domains like this:

for DOMAIN in "${@:$OPTIND:$#}"; do
  DIRECTORY=${DOMAIN}_recon
  echo "Creating directory $DIRECTORY."
  mkdir $DIRECTORY
  # call your scan functions here based on $MODE
done

"${@:$OPTIND:$#}" slices the arguments array from the first domain onward.

#!/bin/bash

PATH_TO_DIRSEARCH="/Users/yourname/tools/dirsearch"

nmap_scan() {
  nmap $DOMAIN > $DIRECTORY/nmap
  echo "The results of nmap scan are stored in $DIRECTORY/nmap."
}

dirsearch_scan() {
  $PATH_TO_DIRSEARCH/dirsearch.py -u $DOMAIN -e php --simple-report=$DIRECTORY/dirsearch
  echo "The results of dirsearch scan are stored in $DIRECTORY/dirsearch."
}

crt_scan() {
  curl "https://crt.sh/?q=$DOMAIN&output=json" -o $DIRECTORY/crt
  echo "The results of cert parsing is stored in $DIRECTORY/crt."
}

getopts "m:" OPTION
MODE=$OPTARG

for DOMAIN in "${@:$OPTIND:$#}"; do
  DIRECTORY=${DOMAIN}_recon
  echo "Creating directory $DIRECTORY."
  mkdir $DIRECTORY
  case $MODE in
    nmap-only)
      nmap_scan
      ;;
    dirsearch-only)
      dirsearch_scan
      ;;
    crt-only)
      crt_scan
      ;;
    *)
      nmap_scan
      dirsearch_scan
      crt_scan
      ;;
  esac

  echo "Generating recon report for $DOMAIN..."
  TODAY=$(date)
  echo "This scan was created on $TODAY" > $DIRECTORY/report

  if [ -f $DIRECTORY/nmap ]; then
    echo "Results for Nmap:" >> $DIRECTORY/report
    grep -E "^\s*\S+\s+\S+\s+\S+\s*$" $DIRECTORY/nmap >> $DIRECTORY/report
  fi

  if [ -f $DIRECTORY/dirsearch ]; then
    echo "Results for Dirsearch:" >> $DIRECTORY/report
    cat $DIRECTORY/dirsearch >> $DIRECTORY/report
  fi

  if [ -f $DIRECTORY/crt ]; then
    echo "Results for crt.sh:" >> $DIRECTORY/report
    jq -r ".[] | .name_value" $DIRECTORY/crt >> $DIRECTORY/report
  fi
done

Writing a function library

As your scripts grow, it’s best to keep your commonly used functions in a separate file, so you can reuse them easily.

Create a file called scan.lib with your scan functions:

#!/bin/bash

nmap_scan() {
  nmap $DOMAIN > $DIRECTORY/nmap
  echo "The results of nmap scan are stored in $DIRECTORY/nmap."
}

dirsearch_scan() {
  $PATH_TO_DIRSEARCH/dirsearch.py -u $DOMAIN -e php --simple-report=$DIRECTORY/dirsearch
  echo "The results of dirsearch scan are stored in $DIRECTORY/dirsearch."
}

crt_scan() {
  curl "https://crt.sh/?q=$DOMAIN&output=json" -o $DIRECTORY/crt
  echo "The results of cert parsing is stored in $DIRECTORY/crt."
}

Then, in your main script, source the library to access these functions:

#!/bin/bash

source ./scan.lib
PATH_TO_DIRSEARCH="/Users/yourname/tools/dirsearch"

# rest of your script here

This way, you keep your main script clean and focused.
Adding an interactive mode

Sometimes, instead of passing domains as arguments, you might want to enter them one by one during execution.

You can add an interactive mode triggered by a flag (like -i), and use a while loop to prompt the user:

while getopts "m:i" OPTION; do
  case $OPTION in
    m)
      MODE=$OPTARG
      ;;
    i)
      INTERACTIVE=true
      ;;
  esac
done

scan_domain() {
  DOMAIN=$1
  DIRECTORY=${DOMAIN}_recon
  echo "Creating directory $DIRECTORY."
  mkdir $DIRECTORY
  case $MODE in
    nmap-only)
      nmap_scan
      ;;
    dirsearch-only)
      dirsearch_scan
      ;;
    crt-only)
      crt_scan
      ;;
    *)
      nmap_scan
      dirsearch_scan
      crt_scan
      ;;
  esac
}

report_domain() {
  DOMAIN=$1
  DIRECTORY=${DOMAIN}_recon
  echo "Generating recon report for $DOMAIN..."
  TODAY=$(date)
  echo "This scan was created on $TODAY" > $DIRECTORY/report
  if [ -f $DIRECTORY/nmap ]; then
    echo "Results for Nmap:" >> $DIRECTORY/report
    grep -E "^\s*\S+\s+\S+\s+\S+\s*$" $DIRECTORY/nmap >> $DIRECTORY/report
  fi
  if [ -f $DIRECTORY/dirsearch ]; then
    echo "Results for Dirsearch:" >> $DIRECTORY/report
    cat $DIRECTORY/dirsearch >> $DIRECTORY/report
  fi
  if [ -f $DIRECTORY/crt ]; then
    echo "Results for crt.sh:" >> $DIRECTORY/report
    jq -r ".[] | .name_value" $DIRECTORY/crt >> $DIRECTORY/report
  fi
}

if [ "$INTERACTIVE" ]; then
  INPUT=""
  while [ "$INPUT" != "quit" ]; do
    echo "Please enter a domain (or type 'quit' to exit):"
    read INPUT
    if [ "$INPUT" != "quit" ]; then
      scan_domain "$INPUT"
      report_domain "$INPUT"
    fi
  done
else
  # process domains from command line as before
fi

Scheduling automatic scans with cron

To automate running your recon regularly, use cron jobs.

Edit your crontab:

crontab -e

Add a line like this to run your script every day at 9:30 PM:

30 21 * * * /path/to/recon.sh example.com

You can also run a script to commit scan results to GitHub and get notifications when new findings appear.
Example usage and explanations

After saving your script (e.g., recon.sh) and making it executable (chmod +x recon.sh), here’s how to use it:
Running a full scan on one domain

./recon.sh example.com

This runs all scans (Nmap, Dirsearch, and crt.sh) against example.com, saving the results in the example.com_recon folder, with a master report summarizing everything.
Running only Nmap scans

./recon.sh example.com nmap-only

This runs just Nmap for the target domain.
Running scans on multiple domains

./recon.sh -m dirsearch-only example.com test.com

This runs Dirsearch scans on both domains sequentially.
Using interactive mode

./recon.sh -i -m nmap-only

The script enters interactive mode, prompting you to enter domains one by one (type quit to exit). It runs Nmap only on each input.
Final tips

Customize tools and flags: You can add more tools, change flags, or add output parsing to suit your recon style.
Schedule scans: Use cron jobs to automate your recon and stay updated on target changes.
Version control: Keep your scripts and output in Git to track changes and collaborate.
Expand the script: Integrate other tools like Amass, Subfinder, or your custom checks.
Learn Bash: The more comfortable you get scripting, the more powerful and flexible your recon process becomes.

Building your own recon tool like this can save you hours, keep your workflow consistent, and give you a deeper understanding of the tools and processes behind recon. With malwaricon as a solid starting point, you can tweak and scale your recon to fit any bug bounty program or pentesting engagement.

Happy hunting!


This content originally appeared on DEV Community and was authored by Marília Rocha