Build a Phishing Site Detector with NextJS and VirusTotal

Build a Phishing Site Detector with NextJS and VirusTotal

When applying for a tech job it is important to have a strong portfolio to impress the company. Especially for beginner developers the first impression of resume and projects are playing crucial role. The most common project types are To-do app, Weather app, Blog and etc. which is boring.

In this new series we will build unique and creative stuff and ship it to production. So, in this episode we’re going to build a simple app that will check phishing risk against provided URL.

We’re going to use a NextJS to build our app fast and ship it to production easily using Vercel. Each time we make a new commit Vercel will update our app so we don’t need to worry about deployment but only focus on building.

Why this project is great?

Because it involves many skills those required to have as a developer:

  • TypeScript.
  • Third-party integration.
  • Use of NextJS itself.
  • UI/UX skills.
  • Tailwind CSS (eh)
  • Deployment

What we are building?

This project is going to be a single page app that will let user to enter a URL. After user enters the URL, it should extract the domain and send it to third-party service which will check the phishing risk of that domain. Then, on the backend we need to evaluate the response from that service and display the result to the user with nice UI/UX interface.

Getting Started

Let’s start by creating a new NextJS project. I will be using latest version of NextJS which is version 14 at time of writing this post.

npx create-next-app@latest

It will prompt few questions to configure the NextJS project. I will select to use TypeScript, initialise the Tailwind CSS and use App router.

✔ What is your project named? … <your_app_name_here>
✔ Would you like to use TypeScript? … No / <u>**Yes**</u>
✔ Would you like to use ESLint? … No / <u>**Yes**</u>
✔ Would you like to use Tailwind CSS? … No / <u>**Yes**</u>
✔ Would you like to use `src/` directory? … <u>**No**</u> / Yes
✔ Would you like to use App Router? (recommended) … No / <u>**Yes**</u>
✔ Would you like to customize the default import alias (@/*)? … No / <u>**Yes**</u>

Now you should have initialised NextJS project ready to use.

Integration with VirusTotal

Virus Total is an online service that analyses suspicious files and URLs to detect types of malware and malicious content using antivirus engines and website scanners.

Create an account on VirusTotal to get an API key for interacting with their service programmatically.

Since we want to check domain safety we had to use Domain Report endpoint to gather deep analyse of the submitted domain.

We can test the endpoint with Postman to see which part of the response we need to calculate the risk of phishing. Here’s the results:

Build a Phishing Site Detector with NextJS and VirusTotal

There are lots of useful information on domain analysis based on various needs. But for now, we will continue with selected part which will help us to calculate the risk.

Building Backend

Since we have all information about what we’re going to build, it’s time to put these knowledge into practice. First, create a new API route that will send request to VirusTotal:

app/api/virustotal/route.ts

import { calcRisk } from '@/utils/calcRiskcomponents'
import axios from 'axios'
import { type NextRequest } from 'next/server'
import { getDomain } from 'tldts'

export async function GET(request: NextRequest) {
    const urlToAnalyze = request.nextUrl.searchParams.get('urlToAnalyze')
    if (!urlToAnalyze) {
        return new Response("URL must not be empty.", {status: 400})
    }
    try {
        const domain = getDomain(urlToAnalyze)
        const virustotalRes = await axios.get(`https://www.virustotal.com/api/v3/domains/${domain}`, {
            headers: {
                "x-apikey": process.env.VIRUSTOTAL_API_KEY
            }
        })
        const stats = calcRisk(virustotalRes?.data?.data?.attributes?.last_analysis_stats)
        return Response.json({ stats: stats })
    } catch (err) {
        return new Response("Internal Server Error", {status: 500})
    }

}

This route will handle the GET request that will be sent from frontend part of our app. Let’s break down the code:

  • Validating if required search param named urlToAnalyze is present. This param should be URL that provided by user.
  • If urlToAnalyze is present then we will extract the domain from that URL using npm module named tldts.
  • Then, based on VirusTotal docs we’re sending the domain to gather analysis report. Also we’re including the API Key in the headers.
  • We have a helper util function calcRisk which will evaluate the result of last_analysis_stats part from the response.

So, the calcRisk is a simple helper function that will determine if URL is risky or not. You can change the logic as you want such as assigning weights to the states of analysis. I would mark the domain “High Risk” even if there are only single report from antivirus engines or scanners.

So here’s the code for better understanding:

utils/calcRisk.ts

interface VirustotalStatsForDomain {
    harmless: number,
    malicious: number,
    suspicious: number,
    undetected: number,
    timeout: number
}

export function calcRisk(virsusTotalStatsForDomain: VirustotalStatsForDomain) {
    const { malicious, suspicious } = virsusTotalStatsForDomain;

    if(malicious > 0) {
        return "High Risk"
    }

    if (suspicious > 0) {
        return "Moderate Risk"
    }

    return "Low Risk"
}

Cool, that’s all for backend. Now, let’s move on to frontend part.

Building Frontend

We’re using app router in our NextJS project, so we should create the pages as <folder-name>/page.tsx which in this case I will create it as check-site/page.tsx. That will be routable on browser as /check-site.

If I would be brutally honest, Tailwind CSS is something that I don’t prefer but it’s useful for small projects like this.

Well, here’s the code for frontend:

/app/check-site/page.tsx

"use client"

import Hero from "@/components/Herocomponents"
import axios from "axios"
import { useState } from "react"

export default function Page() {
    const [url, setUrl] = useState<string>("");
    const [analyzeResult, setAnalyzeResult] = useState<string | null>(null)
    const handleUrlAnalyze = async () => {
      const res = await axios.get(`/api/virustotal?urlToAnalyze=${url}`)
      setAnalyzeResult(res?.data?.stat)
    }

    const icon: any = {
      "Low Risk": <svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 24 24" fill="none" stroke="green" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="lucide lucide-shield-check"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/><path d="m9 12 2 2 4-4"/></svg>,
      "Moderate Risk": <svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 24 24" fill="none" stroke="yellow" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="lucide lucide-shield-alert"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>,
      "High Risk": <svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 24 24" fill="none" stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="lucide lucide-shield-x"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/><path d="m14.5 9-5 5"/><path d="m9.5 9 5 5"/></svg>,
    }

    return (
      <section className="flex flex-col justify-between bg-white dark:bg-gray-900 min-h-screen">
        <div>
          <Hero />
          <div className="px-4 mx-auto max-w-2xl flex flex-col items-center justify-center">
            <div className="w-full flex flex-col items-center">
              <div className="sm:col-span-2 w-full">
                <label className="block mb-2 text-md font-medium text-gray-900 text-center dark:text-white">Domain or URL to Analyze</label>
                <input type="text" name="name" id="name" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" placeholder="Type domain to check"
                onChange={(e) => setUrl(e.target.value)}
                />
              </div>
            </div>
            <button type="button" className="inline-flex items-center px-5 py-2.5 mt-4 sm:mt-6 text-sm font-medium text-center text-white bg-blue-900 rounded-lg hover:bg-primary-800"
            onClick={handleUrlAnalyze}
            >
              Check URL
            </button>
          </div>
          <div className="flex flex-col justify-center items-center w-full py-5">
            {analyzeResult && icon[analyzeResult]}

            <h3 className="text-md">{analyzeResult}</h3>
          </div>
        </div>

        {/* Image container positioned at the bottom */}
        <div className="flex justify-center items-center w-full py-5">
          Made with
          <img className="ml-1" src="https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/VirusTotal_logo.svg/2560px-VirusTotal_logo.svg.png" width={100} alt="VirusTotal logo" />
        </div>
      </section>

    )
  }

The only hidden part in the code is <Hero /> component which is below:

components/Hero.tsx

const Hero = () => {
    return (
        <section className="bg-white dark:bg-gray-900">
            <div className="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16 lg:px-12">
                <h1 className="mb-4 text-4xl font-extrabold tracking-tight leading-none text-gray-900 md:text-5xl lg:text-6xl dark:text-white">Secure Your Surfing</h1>
                <p className="mb-8 text-lg font-normal text-gray-500 lg:text-xl sm:px-16 xl:px-48 dark:text-gray-400">Navigate the web with confidence using instant site safety checker.</p>
                <div className="flex justify-center">
                    <img className="h-auto max-w-full" src="logo.png" width={200} alt="image description" />
                </div>
            </div>
        </section>
    )
}

export default Hero;

And yes, I generated the tailwind code with ChatGPT.

Result

Now, we have full working, single feature app that will analyse the phishing risk of submitted URL. Here’s how it looks when you run the app:

Build a Phishing Site Detector with NextJS and VirusTotal

Build a Phishing Site Detector with NextJS and VirusTotal

What’s next?

You need to deploy your NextJS to Vercel to make it publicly accessible for others. Also, remember to explain your solution well in ReadMe file since it shows that you’re documenting your work.

Cross-Published with OnePublish.