Learn in Public unlocks on Jan 1, 2026

This lesson will be public then. Admins can unlock early with a password.

Build Your First Security Tool in Rust (Beginner-Friendly 2026 Guide)
Learn Cybersecurity

Build Your First Security Tool in Rust (Beginner-Friendly 2026 Guide)

Step-by-step tutorial to build a simple Rust security scanner using Tokio and Reqwest, then ship it like a pro.

rust security tooling tokio async rust blue team engineering

Write and ship a real Rust security tool end to end: a safe URL liveness checker with concurrency limits, JSON logs, validation, and cleanup.

What You’ll Build

  • A small Rust CLI that checks URLs concurrently with strict timeouts.
  • JSON output for SOC ingestion and predictable, tagged User-Agent.
  • Safe defaults (low concurrency, delays) to avoid accidental abuse.

Prerequisites

  • macOS or Linux with Rust 1.80+ (rustc --version).
  • Python 3.10+ (optional) to host a local test page.
  • Run only against authorized URLs; use localhost for practice.
  • Do not scan third-party sites without written approval.
  • Keep concurrency low; stop on rate limits or complaints.
  • Never hardcode secrets; keep configs outside binaries.

Step 1) Prepare test targets (local)

Click to view commands
mkdir -p mock_web
echo "ok" > mock_web/index.html
python3 -m http.server 8020 --directory mock_web > mock_web/server.log 2>&1 &
Validation: `curl -I http://127.0.0.1:8020/` returns `200 OK`.

Common fix: If port 8020 is used, change to a free port and reuse it below.

Step 2) Create the Rust project

Click to view commands
cargo new rustsec-checker
cd rustsec-checker
Validation: `ls` shows `Cargo.toml` and `src/main.rs`.

Step 3) Add dependencies

Replace Cargo.toml with:

Click to view toml code
[package]
name = "rustsec-checker"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1.40", features = ["full"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
futures = "0.3"
indicatif = "0.17"
anyhow = "1.0"
Validation: `cargo check` should succeed after adding the code.

Step 4) Implement the tool

Replace src/main.rs with:

Click to view Rust code
use clap::Parser;
use futures::stream::{self, StreamExt};
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::Client;
use serde::Serialize;
use std::time::Duration;

#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
    /// File containing URLs (one per line)
    #[arg(long)]
    file: String,
    /// Max concurrent requests
    #[arg(long, default_value_t = 5)]
    concurrency: usize,
    /// Delay between tasks in ms
    #[arg(long, default_value_t = 50)]
    delay_ms: u64,
    /// Request timeout in seconds
    #[arg(long, default_value_t = 5)]
    timeout: u64,
}

#[derive(Serialize)]
struct ResultRow {
    url: String,
    status: Option<u16>,
    ok: bool,
    elapsed_ms: u128,
}

async fn check_url(client: &Client, url: &str, delay_ms: u64) -> ResultRow {
    let start = std::time::Instant::now();
    let resp = client.get(url).send().await;
    let (status, ok) = match resp {
        Ok(r) => (Some(r.status().as_u16()), r.status().is_success()),
        Err(_) => (None, false),
    };
    tokio::time::sleep(Duration::from_millis(delay_ms)).await;
    ResultRow {
        url: url.to_string(),
        status,
        ok,
        elapsed_ms: start.elapsed().as_millis(),
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    let body = std::fs::read_to_string(&args.file)?;
    let targets: Vec<String> = body.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).map(|s| s.to_string()).collect();

    let client = Client::builder()
        .user_agent("rustsec-checker (+you@example.com)")
        .timeout(Duration::from_secs(args.timeout))
        .build()?;

    let pb = ProgressBar::new(targets.len() as u64);
    pb.set_style(
        ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] {pos}/{len} {msg}")?
            .progress_chars("#>-"),
    );

    let results: Vec<ResultRow> = stream::iter(targets)
        .map(|url| {
            let client = client.clone();
            let delay = args.delay_ms;
            async move {
                let row = check_url(&client, &url, delay).await;
                row
            }
        })
        .buffer_unordered(args.concurrency)
        .inspect(|_| pb.inc(1))
        .collect()
        .await;

    pb.finish_with_message("done");
    println!("{}", serde_json::to_string_pretty(&results)?);
    Ok(())
}
Validation:
Click to view commands
echo -e "http://127.0.0.1:8020/\nhttps://example.com" > urls.txt
cargo run -- --file urls.txt --concurrency 3 --delay-ms 25
Expected: JSON array with statuses (200 for localhost, 200 for example.com).

Common fixes:

  • connection refused: ensure the local server is running.
  • Timeouts: increase --timeout for slow targets or reduce concurrency.

Step 5) Ship responsibly

  • Default to safe values: low concurrency, small timeouts, optional jitter.
  • Log JSON to stdout; pipe to files for audits: ... | tee results.json.
  • Add --retry only for idempotent GETs; never hammer login pages.
  • Sign binaries or distribute checksums; document usage and authorized scopes.

Cleanup

Click to view commands
cd ..
pkill -f "http.server 8020" || true
rm -rf rustsec-checker mock_web
Validation: `lsof -i :8020` should show no listener; project folder removed.

Quick Reference

  • Keep scans in-scope, low and slow, with clear User-Agent tagging.
  • Validate after each change; capture JSON for SOC ingestion.
  • Avoid secrets in code; keep configs external and rotated.

Similar Topics

FAQs

Can I use these labs in production?

No—treat them as educational. Adapt, review, and security-test before any production use.

How should I follow the lessons?

Start from the Learn page order or use Previous/Next on each lesson; both flow consistently.

What if I lack test data or infra?

Use synthetic data and local/lab environments. Never target networks or data you don't own or have written permission to test.

Can I share these materials?

Yes, with attribution and respecting any licensing for referenced tools or datasets.