net/go.book
All Parts Marketing

Building Simple Security Tools in Go

"A carpenter doesn't buy a hammer for every job — sometimes the fastest fix is the one you make yourself, sized exactly to the problem in front of you."


Why Roll Your Own

Professional-grade security suites (Nessus, Burp Suite, Nmap) exist for a reason, and nothing here is meant to replace them. But a huge amount of day-to-day security work is small, specific, and repetitive — "check every service in our fleet for this one misconfiguration," "verify this list of hosts still has X open," "confirm none of our public endpoints regressed on this header." That's exactly the kind of task where a 50-line Go program you fully understand beats reaching for a heavyweight tool with a learning curve.

Go is particularly well suited to this: a single static binary, a standard library that already covers HTTP, TLS, and TCP/UDP sockets, and goroutines that make "check a thousand things concurrently" nearly free to write.


Design Goals for a Small Security Tool

Whatever the tool checks, a few habits make it trustworthy and reusable:

  • Read-only by default. A tool that only observes and reports is safe to run broadly; one that mutates state needs much more caution and explicit opt-in.
  • Bounded and polite. Timeouts on every network call, concurrency limits, and respect for the target — the tool itself shouldn't become a denial-of-service source (Chapter 5.3).
  • Structured output. Even a simple tool should produce output another program (or a human scanning a report) can parse reliably.
  • Fail loud, not silent. A check that errors out should say so clearly rather than being indistinguishable from "passed."

Go Implementation: An HTTP Security Headers Auditor

A genuinely useful, small tool: check whether a web endpoint sets the HTTP response headers that meaningfully reduce common web-client attack surface (clickjacking, MIME-sniffing, protocol downgrade). This complements the scanner from Chapter 5.4 and the sniffer from Chapter 5.5 without duplicating either — it works entirely at the HTTP layer.

package main

import (
	"fmt"
	"net/http"
	"os"
	"time"
)

// headerCheck describes one security-relevant header and how to judge it.
type headerCheck struct {
	Name        string
	Description string
	Required    bool
}

var checks = []headerCheck{
	{
		Name:        "Strict-Transport-Security",
		Description: "forces browsers to use HTTPS on future visits",
		Required:    true,
	},
	{
		Name:        "X-Content-Type-Options",
		Description: "prevents MIME-type sniffing (expect \"nosniff\")",
		Required:    true,
	},
	{
		Name:        "X-Frame-Options",
		Description: "mitigates clickjacking via framing",
		Required:    false,
	},
	{
		Name:        "Content-Security-Policy",
		Description: "restricts which sources can execute/load content",
		Required:    false,
	},
}

// audit fetches the target URL and reports which security headers are present.
func audit(url string) error {
	client := http.Client{Timeout: 5 * time.Second}

	resp, err := client.Get(url)
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	fmt.Printf("Auditing %s (status %d)\n", url, resp.StatusCode)
	for _, c := range checks {
		value := resp.Header.Get(c.Name)
		status := "MISSING"
		if value != "" {
			status = "present: " + value
		}
		flag := ""
		if value == "" && c.Required {
			flag = "  <-- recommended"
		}
		fmt.Printf("  %-28s %s%s\n", c.Name, status, flag)
	}
	return nil
}

func main() {
	if len(os.Args) != 2 {
		fmt.Fprintln(os.Stderr, "usage: headercheck <https://target-url>")
		os.Exit(1)
	}
	if err := audit(os.Args[1]); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}

Points worth understanding:

  • http.Client{Timeout: ...} ensures a slow or hanging endpoint can't stall the tool indefinitely — an essential habit for anything that reaches out over the network.
  • Reading resp.Header.Get(...) is all it takes to check a header's presence; the tool never needs to touch the response body to do this particular job.
  • The tool is read-only — a single GET — so it's safe to run against your own services routinely, even in CI, without any risk of side effects.

This same skeleton — fetch, inspect, report — extends naturally: check for cookies missing the Secure or HttpOnly attributes, verify a redirect chain always ends on HTTPS, or confirm a certificate's expiry date is comfortably in the future (which ties directly into Chapter 5.12's certificate management material).


Extending the Pattern: Concurrent Fleet Checks

The real payoff of writing this in Go shows up once you need to check many targets, not one:

func auditAll(urls []string) {
	var wg sync.WaitGroup
	for _, u := range urls {
		wg.Add(1)
		go func(u string) {
			defer wg.Done()
			if err := audit(u); err != nil {
				fmt.Fprintf(os.Stderr, "%s: %v\n", u, err)
			}
		}(u)
	}
	wg.Wait()
}

(This snippet assumes "sync" is imported alongside the packages above.) A fleet of a hundred endpoints that would take minutes to check sequentially finishes in roughly the time of the single slowest request — the same concurrency pattern used by the scanner in Chapter 5.4.


Frequently Asked Questions

Why write a headers auditor instead of just using Burp Suite or Nessus? Both of those tools do this and far more, but they're heavyweight for a question this narrow — "does our fleet still set HSTS after last week's config change" doesn't need a full scanning suite spun up, it needs a fifty-line program you can read top to bottom and trust completely. The point of this chapter isn't to replace professional tooling, it's to recognize when a small, purpose-built script is genuinely the faster and more auditable choice.

Is it safe to point this tool at a website I don't control? A single read-only GET against a public site's headers is about as low-risk as network probing gets, but "low-risk" isn't the same as "always fine" — running it against your own services, a lab target, or anything you're explicitly authorized to test is the safe default this book sticks to throughout. If you're ever unsure whether checking a third party's headers crosses a line, treat that uncertainty as your answer and stick to systems you own or have permission for.

What happens if the target is slow or just never responds? That's exactly what http.Client{Timeout: 5 * time.Second} is guarding against — without it, a single hanging endpoint could stall the whole audit indefinitely, which is especially dangerous once you're checking a fleet of a hundred targets concurrently. If you see the tool hang despite the timeout, double check that you're using the client's timeout rather than relying on context cancellation you never wired up, since the two are easy to conflate.

A header shows as MISSING but I'm sure the server sends it — what's going on? Check for case sensitivity and proxying first: resp.Header.Get is case-insensitive so that's rarely the culprit, but a reverse proxy, CDN, or load balancer in front of the real server can silently strip or rewrite headers before they ever reach your client. Re-run the check directly against the origin server if you can, and compare — a header that's present at the origin but missing at the edge is itself a useful finding.


Where a Tool Like This Fits

Small, purpose-built tools like this one are exactly what feed a penetration test's reconnaissance phase (Chapter 5.8), what a security team runs on a schedule to catch configuration drift, and what an incident responder reaches for when they need a very specific answer right now rather than waiting on a heavyweight scanner's next scheduled run.

The best security tool is often the smallest one that answers exactly the question you're asking — nothing more, and nothing you don't fully understand.