Build a Web Based TCP Port Scanner

HTMLHTMLBeginner
Practice Now

Introduction

In the previous project, we developed a Python port scanner that leveraged threading and sockets to scan TCP ports. While effective, there's room for improvement using third-party packages.

In this project, we'll enhance our port scanner by integrating the python-nmap library, offering more robust scanning capabilities. Furthermore, we'll build a web application using Flask to provide a user-friendly interface for our scanner. This step-by-step project will guide you through the process, ensuring you can follow along and build upon your existing knowledge.

👀 Preview

🎯 Tasks

In this project, you will learn:

  • How to set up a Flask project and organize its structure
  • How to use Flask-WTF to create and handle web forms securely
  • How to implement Flask routes to handle web page requests and submissions
  • How to utilize the Nmap library in Python to conduct port scans
  • How to display scan results dynamically on a webpage using Flask and HTML templates
  • How to apply basic Tailwind CSS to enhance the frontend design

🏆 Achievements

After completing this project, you will be able to:

  • Demonstrate a foundational understanding of web development with Flask, including routing, template rendering, and form handling
  • Apply practical experience with integrating Python scripts with web interfaces
  • Exhibit proficiency in using the Nmap library for network scanning tasks
  • Utilize Flask-WTF for form creation and validation in a web application
  • Showcase familiarity with using Tailwind CSS for styling web pages and enhancing user interface design
  • Create a functional web-based application that interacts with backend Python scripts to perform network scans

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL flask(("`Flask`")) -.-> flask/CoreConceptsGroup(["`Core Concepts`"]) flask(("`Flask`")) -.-> flask/DataHandlingGroup(["`Data Handling`"]) flask(("`Flask`")) -.-> flask/DevelopmentToolsGroup(["`Development Tools`"]) html(("`HTML`")) -.-> html/BasicStructureGroup(["`Basic Structure`"]) html(("`HTML`")) -.-> html/TextContentandFormattingGroup(["`Text Content and Formatting`"]) html(("`HTML`")) -.-> html/MultimediaandGraphicsGroup(["`Multimedia and Graphics`"]) html(("`HTML`")) -.-> html/TablesGroup(["`Tables`"]) html(("`HTML`")) -.-> html/FormsandInputGroup(["`Forms and Input`"]) html(("`HTML`")) -.-> html/AdvancedElementsGroup(["`Advanced Elements`"]) python(("`Python`")) -.-> python/ControlFlowGroup(["`Control Flow`"]) python(("`Python`")) -.-> python/DataStructuresGroup(["`Data Structures`"]) python(("`Python`")) -.-> python/ModulesandPackagesGroup(["`Modules and Packages`"]) flask/CoreConceptsGroup -.-> flask/application_object("`Application Object`") flask/DataHandlingGroup -.-> flask/incoming_request_data("`Incoming Request Data`") flask/DevelopmentToolsGroup -.-> flask/template_rendering("`Template Rendering`") flask/CoreConceptsGroup -.-> flask/configuration("`Configuration`") flask/CoreConceptsGroup -.-> flask/url_route_registrations("`URL Route Registrations`") html/BasicStructureGroup -.-> html/basic_elems("`Basic Elements`") html/BasicStructureGroup -.-> html/charset("`Character Encoding`") html/BasicStructureGroup -.-> html/lang_decl("`Language Declaration`") html/BasicStructureGroup -.-> html/viewport("`Viewport Declaration`") html/BasicStructureGroup -.-> html/head_elems("`Head Elements`") html/TextContentandFormattingGroup -.-> html/text_head("`Text and Headings`") html/MultimediaandGraphicsGroup -.-> html/embed_media("`Embedding External Media`") html/TablesGroup -.-> html/tables("`Table Structure`") html/FormsandInputGroup -.-> html/forms("`Form Elements`") html/AdvancedElementsGroup -.-> html/inter_elems("`Interactive and Dynamic Elements`") html/AdvancedElementsGroup -.-> html/templating("`HTML Templating`") python/ControlFlowGroup -.-> python/conditional_statements("`Conditional Statements`") python/ControlFlowGroup -.-> python/for_loops("`For Loops`") python/DataStructuresGroup -.-> python/lists("`Lists`") python/ModulesandPackagesGroup -.-> python/importing_modules("`Importing Modules`") subgraph Lab Skills flask/application_object -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} flask/incoming_request_data -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} flask/template_rendering -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} flask/configuration -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} flask/url_route_registrations -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} html/basic_elems -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} html/charset -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} html/lang_decl -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} html/viewport -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} html/head_elems -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} html/text_head -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} html/embed_media -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} html/tables -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} html/forms -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} html/inter_elems -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} html/templating -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} python/conditional_statements -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} python/for_loops -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} python/lists -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} python/importing_modules -.-> lab-298837{{"`Build a Web Based TCP Port Scanner`"}} end

Implementing the Index Page

To get started, open templates/index.html and add the following code to ensure the form is correctly set up for submitting scan requests:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TCP Port Scanner</title>
    <link
      href="https://cdn.jsdelivr.net/npm/tailwindcss@2.0.3/dist/tailwind.min.css"
      rel="stylesheet"
    />
    <script>
      function updateButton() {
        var submitButton = document.getElementById("scanButton");
        submitButton.value = "Scanning...";
        submitButton.classList.add("cursor-not-allowed", "opacity-50");
        submitButton.disabled = true;
      }
    </script>
  </head>
  <body class="bg-gray-100 flex items-center justify-center h-screen">
    <div class="bg-white p-8 rounded-lg shadow-md">
      <h1 class="text-2xl font-bold mb-4">TCP Port Scanner</h1>
      <form action="" method="post" class="space-y-4" onsubmit="updateButton()">
        {{ form.hidden_tag() }}
        <div>
          {{ form.host.label(class="block text-sm font-medium text-gray-700") }}
          {{ form.host(class="mt-1 block w-full rounded-md border-gray-300
          shadow-sm focus:border-indigo-500 focus:ring-indigo-500") }}
        </div>
        <div>
          {{ form.ports.label(class="block text-sm font-medium text-gray-700")
          }} {{ form.ports(class="mt-1 block w-full rounded-md border-gray-300
          shadow-sm focus:border-indigo-500 focus:ring-indigo-500") }}
        </div>
        <div>
          {{ form.submit(id="scanButton", class="w-full flex justify-center py-2
          px-4 border border-transparent rounded-md shadow-sm text-sm
          font-medium text-white bg-indigo-600 hover:bg-indigo-700
          focus:outline-none focus:ring-2 focus:ring-offset-2
          focus:ring-indigo-500") }}
        </div>
      </form>
    </div>
  </body>
</html>

The index.html template provides a user-friendly interface for submitting a host and port range for scanning. The HTML form includes two main fields: one for the host address and another for specifying the port range.

Flask-WTF, a Flask extension for working with WTForms, is used to render these fields in the template. The form submission is enhanced by a small JavaScript function updateButton() that changes the text of the submit button to "Scanning..." upon form submission. This visual feedback informs the user that their scan request is being processed.

The page styling is handled by Tailwind CSS, a utility-first CSS framework that enables rapid UI development.

Implementing the Scan Results Page

Open templates/results.html and ensure the following code is included to display the scan results:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Scan Results</title>
    <link
      href="https://cdn.jsdelivr.net/npm/tailwindcss@2.0.3/dist/tailwind.min.css"
      rel="stylesheet"
    />
  </head>
  <body class="bg-gray-100">
    <div class="flex flex-col items-center justify-center min-h-screen">
      <div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-4xl">
        <h1 class="text-2xl font-bold mb-4 text-center">
          Scan Results for {{ host }}
        </h1>
        <div class="overflow-x-auto">
          <table class="table-auto w-full text-left whitespace-no-wrap">
            <thead>
              <tr
                class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50"
              >
                <th class="px-4 py-3">PORT</th>
                <th class="px-4 py-3">STATE</th>
                <th class="px-4 py-3">SERVICE</th>
                <th class="px-4 py-3">VERSION</th>
              </tr>
            </thead>
            <tbody
              class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800"
            >
              {% for result in scan_results %}
              <tr class="text-gray-700 dark:text-gray-400">
                <td class="px-4 py-3 text-sm">{{ result.port }}/tcp</td>
                <td class="px-4 py-3 text-sm">{{ result.state }}</td>
                <td class="px-4 py-3 text-sm">{{ result.name }}</td>
                <td class="px-4 py-3 text-sm">
                  {{ result.product }} {{ result.version }} {{ result.extra }}
                </td>
              </tr>
              {% endfor %}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </body>
</html>

The results.html template is responsible for displaying the outcome of the port scan. It presents the scan results in a table format, listing each port with its corresponding state, service name, and service version if available.

This template uses Tailwind CSS for styling, ensuring the results page is both responsive and visually appealing. The use of Jinja2 templating engine (integrated with Flask) allows for dynamic content rendering, where the scan results are passed from the Flask application to the template and iterated over to populate the table.

Init the Flask Application

In this step, we'll create the main Python script for our Flask application.

Add the following code in app.py:

## Import necessary modules
from flask import Flask, render_template, request, redirect, url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Regexp
import nmap

## Initialize Flask app
app = Flask(__name__)
app.config['SECRET_KEY'] = 'labex'

## Initialize Nmap PortScanner
nm = nmap.PortScanner()

## Define Flask-WTF form for scanning inputs
class ScanForm(FlaskForm):
    host = StringField('Host', validators=[DataRequired()])
    ports = StringField('Port Range', validators=[DataRequired(), Regexp(r'^\d+-\d+$', message="Format must be start-end")])
    submit = SubmitField('Scan')

In this step, the Flask application and the form class are initialized.

The app.py script begins by importing necessary modules, including Flask itself, Flask-WTF for form handling, and Nmap for conducting port scans. The Flask application instance is created and configured with a secret key to protect against CSRF attacks.

The ScanForm class defines the form fields for host and port range inputs, using validators to ensure that the data is provided and in the correct format (for the port range, specifically).

Replace 'your_secret_key' with a real secret key that's used to protect the forms against CSRF attacks.

Handling the Index Route

In this step, we'll handle the index route where users can submit the host and port range they wish to scan. Add the following function to app.py:

## Define route for the index page
@app.route('/', methods=['GET', 'POST'])
def index():
    form = ScanForm()  ## Instantiate the form
    if form.validate_on_submit():
        ## Get data from the form
        host = form.host.data
        ports = form.ports.data  ## Format: "start-end"
        ## Redirect to the scan route with form data
        return redirect(url_for('scan', host=host, ports=ports))
    ## Render the index page template with the form
    return render_template('index.html', form=form)

This portion of the app.py script defines the route for the index page of the web application. The index() function renders the index.html template along with the ScanForm instance.

When the form is submitted and passes validation checks, the function redirects the user to the scan route, passing along the form data (host and port range) via URL parameters. This redirection initiates the scanning process.

Implementing the Scan Route

This step involves creating a route to perform the actual scanning and display the results. Add the following function to app.py:

## Define route for the scan results
@app.route('/scan')
def scan():
    ## Retrieve host and ports from the query string
    host = request.args.get('host')
    ports = request.args.get('ports')
    ## Perform the scan using Nmap
    nm.scan(hosts=host, ports=ports, arguments='-sV')  ## -sV for service/version detection
    scan_results = []

    ## Process scan results and store them in a list
    for host in nm.all_hosts():
        for proto in nm[host].all_protocols():
            lport = nm[host][proto].keys()
            for port in lport:
                service = nm[host][proto][port]
                scan_results.append({
                    'port': port,
                    'state': service['state'],
                    'name': service.get('name', 'Unknown'),
                    'product': service.get('product', ''),
                    'version': service.get('version', ''),
                    'extra': service.get('extrainfo', '')
                })

    ## Render the results page template with the scan results
    return render_template('results.html', scan_results=scan_results, host=host)

The scan() function handles the route responsible for performing the actual port scan and displaying the results. It retrieves the host and port range from the query string parameters passed in the URL.

Using the Nmap PortScanner instance, it conducts a scan on the specified host and ports, with -sV argument to detect service versions.

The scan results are processed and organized into a list of dictionaries, each containing details about a scanned port. These details are then passed to the results.html template, where they are displayed to the user.

Running the Flask Application

With all the components in place, you're now ready to run the Flask application and bring your TCP port scanner to life. The final piece of code needed in your app.py ensures that the Flask application will only run if the script is executed directly, not if it's imported as a module in another script. This is a common pattern in Python applications that include a runnable script.

Place the following code snippet at the end of your app.py file:

if __name__ == '__main__':
    app.run(debug=True, port=8080, host='0.0.0.0')

This code tells Flask to start your application with debugging enabled, making it easier to track down errors. The application will listen on all network interfaces (host='0.0.0.0') and use port 8080. Debug mode should only be used during development, as it can be insecure to use in a production environment.

To run your Flask application, make sure you're in the project directory where app.py is located. Then, execute the following command in terminal:

python app.py

Switch to Web 8080 tab to access the TCP port scanner. You can now enter a host and a port range to scan, and view the results on the results page.

The ports 22 and 3306 are ubiquitously associated with SSH and MySQL services, respectively, the port 3000 is utilized for the WebIDE environments.

Summary

In this project, you've learned how to build a simple yet powerful web-based TCP port scanner using Flask and Nmap. We started by setting up the project environment and installing necessary dependencies. We then progressed through creating the Flask application, handling form submissions, performing the port scan, and displaying the results in a user-friendly manner. This project serves as a great introduction to web development with Flask and network scanning with Nmap, offering a practical application that combines both skills.

Other HTML Tutorials you may like