Skip to content

Module 05 — Building CLI Tools

Type 9 · Tool-Build — wrap the module-04 enrichment function into a production-grade ioc-check CLI with typer: subcommands, typed arguments, --help, exit codes, and clear errors for bad input. The archetypal composable command-line tool. Go to the hands-on lab →

Last reviewed: 2026-06

Python for Securitya tool that takes arguments and has a --help is a tool someone else can use.

Difficulty: Beginner  ·  Estimated time: ~3–4 hrs (study + lab)  ·  Prerequisites: Foundations

In 60 seconds

A script with hardcoded paths is a personal tool; a CLI with --help, typed arguments, clean errors, and honest exit codes is a team asset. typer makes the function signature be the argument parser — type hints drive the validation and the help text. The design rule for security CLIs is be explicit about failure, never silent: say what failed and which value, exit non-zero when you fail, and keep subcommands composable so they pipe together like Unix tools.

Why this matters

A script with hardcoded paths and no usage string is a personal tool. A CLI with documented arguments, sensible defaults, --help output, and clean error messages is a team asset. Most security tooling is command-line-first — that's the interface incident responders reach for under pressure — and the difference between a script someone can use and one they have to modify to run is the argument layer.

Objective

Wrap the enrichment function from module 04 into a production-grade ioc-check CLI tool using typer, with subcommands, typed arguments, --help output, and clear error messages for bad input.

The core idea

There are two generations of Python CLI tools: argparse (standard library, explicit, verbose, but always available) and typer (third-party, based on type annotations, generates help text automatically, much less boilerplate). Both are correct choices; typer is the modern default for new tools because the function signature is the argument parser — you add a type hint, and typer adds the validation, the help text, and the error message automatically.

The mental model

In typer the function signature is the argument spec — add a type hint and you get validation, help text, and the error message for free. argparse is the always-available standard-library fallback for when you can't add a dependency; both are correct, typer is the modern default.

The design principle for security CLI tools is: be explicit about failure modes, never silent. If an argument is missing, say what is missing. If a file doesn't exist, say the file path and why. If the API returns an error, print the status code and the URL. A tool that exits with 1 and an unhelpful empty error message is harder to debug than one that exits with 1 and "API returned 429 for 8.8.8.8 — you've hit the rate limit." The extra line of error handling is an hour saved the next time someone runs the tool in a hurry.

The gotcha

A script that exits 0 when it failed is a lie to the caller — and in shell pipelines and CI, exit codes are the interface. A failed enrichment that exits 0 will not fail the CI gate that runs it. Use raise typer.Exit(code=1) or sys.exit(1) explicitly; never rely on an exception accidentally propagating to a non-zero exit.

Exit codes matter. 0 = success; 1 = usage error or runtime failure.

Subcommands are the pattern for tools that do related but distinct things: ioc-check enrich 8.8.8.8, ioc-check report --input enriched.json, ioc-check version. typer handles this with app.command() decorators. Keep subcommands composable — each should read from files or stdin and write to files or stdout, so they can be piped together in a shell one-liner. This is the Unix philosophy applied to security tooling, and it is why grep | awk | sort | uniq -c still beats most GUI dashboards for exploratory analysis.

AI caveat

Ask a model to wrap your enrich.py in a typer app and it nails the structure instantly. Check the three things it tends to skip: does it validate the IOC format before calling the API, does it exit 1 on API failure, and does --help actually describe what the tool does? Those cover most of what makes a CLI usable under pressure.

Learn (~2 hrs)

Typer fundamentals (~1 hr) - Typer — Build great CLIs (official tutorial) — work through the "First Steps", "CLI Arguments", "CLI Options", and "Commands" sections; this covers everything in the lab.

CLI UX for security tools (~30 min) - Command Line Interface Guidelines — clig.dev — the best single reference for CLI design; read the "Basics", "Output", and "Errors" sections; this is what separates a frustrating tool from a well-designed one.

argparse (the standard library fallback) (~30 min) - argparse — Parser for command-line options (Python docs) — focus on add_argument, type, choices, required, and default; you'll reach for this when you can't add third-party dependencies.

Key concepts

  • typer function signatures as the argument spec — type hints drive validation and help text
  • Exit codes as the interface for shell pipelines and CI: 0 = success, 1 = failure
  • Explicit error messages: what failed, which value, what to try
  • Subcommands for related operations: enrich, report, version
  • Reading from files or stdin, writing to stdout or files — composable by design

AI acceleration

Ask a model to turn your enrich.py function into a typer app. It will get the basic structure right instantly. Check the error handling: does it validate the IOC format before calling the API? Does it exit 1 on API failure? Does the --help output actually describe what the tool does? These three questions cover most of what makes a CLI tool usable under pressure.

Check yourself

  • In typer, what supplies the argument validation and the --help text?
  • Why is a tool that exits 0 after a failed run actively dangerous in a CI pipeline?
  • What does it mean for subcommands to be "composable," and why does it matter?

Comments

Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).