Skip to content

2026

Using pytest to Catch CLI Changes

When building CLI apps, it's important to update the major/minor version when the CLI changes (see the semantic versioning philosophy).

How can we ensure we catch these changes with pytest?

syrupy is a great solution.

Let's look at an example for a python CLI that uses typer:

tests/test_cli.py
import pytest
import typer
from typer.testing import CliRunner
from myapp.cli import app

runner = CliRunner()
commands = list(typer.main.get_group(app).commands.keys())

@pytest.mark.parametrize("cmd", commands)
def test_cli(snapshot, cmd: str):
result = runner.invoke(app, [cmd, "--help"], env={"COLUMNS": "120"})
assert result.exit_code == 0
assert result.stdout == snapshot

Now run pytest --snapshot-update.

This will automatically take whatever you compared snapshot to (in this case result.stdout) and create a copy of that in your tests directory. Now when you run pytest, it compares it to these snapshots.

Whenever the CLI changes, it will fail this test until you run pytest --snapshot-update again.

Vibe Coding, Copilot, and Hash Length Extension Attacks

Everyone's talking about fable so I decided the bite the bullet and get a subscription to try it out.

But before trying out fable, I wanted to see how much Copilot could accomplish.

I recently spent most of a week learning more about cryptographic vulnerabilities in computers, so I decided to test out copilot by building a python library and CLI tool to perform hash length extension attacks. There's already a couple projects1 on github that perform this attack, but they all were either no longer maintained, messy, or didn't support all the hashes susceptible to this attack2.

After $3 of credits and an hour of prompting and coddling copilot, it pumped out hashle:

$ hashle --help
                                                                                
 Usage: hashle [OPTIONS] COMMAND [ARGS]...                                      
                                                                                
 Perform hash length extension attacks against vulnerable Merkle-Damgard        
 hashes.                                                                        

╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --help          Show this message and exit.                                  │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ list-algorithms  List every hash algorithm hashle supports.                  │
│ hash             Compute the digest of DATA, mainly useful for generating    │
│                  test signatures.                                            │
│ extend           Perform a hash length extension attack.                     │
╰──────────────────────────────────────────────────────────────────────────────╯
$ hashle list-algorithms
md4          digest=128 bits  block=64 bytes
md5          digest=128 bits  block=64 bytes
ripemd160    digest=160 bits  block=64 bytes
sha          digest=160 bits  block=64 bytes
sha1         digest=160 bits  block=64 bytes
sha256       digest=256 bits  block=64 bytes
sha512       digest=512 bits  block=128 bytes
sm3          digest=256 bits  block=64 bytes
tiger192v1   digest=192 bits  block=64 bytes
tiger192v2   digest=192 bits  block=64 bytes
whirlpool    digest=512 bits  block=64 bytes
$ hashle extend --help
                                                                                
 Usage: hashle extend [OPTIONS]                                                 
                                                                                
 Perform a hash length extension attack.                                        

 Computes the message an attacker would send, and the signature it              
 produces, for every combination of requested hash algorithm(s) and             
 secret length(s).                                                              

╭─ Options ────────────────────────────────────────────────────────────────────╮
│ *  --signature             -s      <str>   Known signature of secret+data,   │
│                                            in hex.                           │
│                                            [required]                        │
│    --data                  -d      <str>   The original known string.        │
│    --file                          <path>  Read the original known string    │
│                                            from a file.                      │
│    --data-format                   <str>   Format of --data: 'raw' or 'hex'. │
│                                            [default: raw]                    │
│    --append                -a      <str>   Data to append.                   │
│    --append-file                   <path>  Read data to append from a file.  │
│    --append-format                 <str>   Format of --append: 'raw' or      │
│                                            'hex'.                            │
│                                            [default: raw]                    │
│ *  --format                -f      <str>   Hash algorithm(s) to target.      │
│                                            Repeat for multiple, or pass      │
│                                            'all'.                            │
│                                            [required]                        │
│    --secret-length         -l      <int>   Assumed length of the secret, in  │
│                                            bytes.                            │
│    --secret-min                    <int>   Minimum secret length to try.     │
│    --secret-max                    <int>   Maximum secret length to try.     │
│    --out-data-format               <str>   Format for the forged message     │
│                                            output.                           │
│                                            [default: raw]                    │
│    --out-signature-format          <str>   Format for the new signature      │
│                                            output.                           │
│                                            [default: hex]                    │
│    --quiet                 -q              Only print the new signature and  │
│                                            data.                             │
│    --help                                  Show this message and exit.       │
╰──────────────────────────────────────────────────────────────────────────────╯

Tip

Also today I learned about markdown-exec, my new favorite project that lets you use shell or python code to render docs, which is how the hashle output above is displayed.

For $10/month, copilot's not bad.


  1. See hash_extender and hlextend. ↩

  2. That is, hashes that use Merkle-Damgard hash constructions. ↩

A Normie Runner's Review of Runna

TL;DR

I think it's worth it.

I was quite on the fence about paying for Runna when I signed up for to train for my thrid marathon; I hope this review will help other decided whether or not to purchase a subscription.

TL;DR

  • I found out a few months ago the Navy scrapped the new performance evaluation app meant to replace the current one built in 1998 due to excessive bugs and software issues.
  • I was disappointed it failed, because the 1998 app is quite bad.
  • I'd been thinking about a foray into desktop app development, so I took the opportunity to build a replacement for NAVFIT98.

ReVa

ReVa is "A Ghidra MCP server for AI-powered reverse engineering". It has two modes:

  • Assistant Mode: for interactive RE in the ghidra GUI.
  • Headless Mode: for use in headless ghidra scripts.

Installation

The ReVa README install instructions aren't as clear as they could be, so here are the straightforward install instructions:

  1. Download the latest release of ReVa.
  2. In the Ghidra Project view, got the File > Install Extensions
  3. Click the green "+" at the top right.
  4. Select the ReVa zip file you downloaded.
  5. Restart Ghidra
  6. In the Ghidra Project view, go to File > Configure
  7. Click the plug icon in the top right.
  8. Check the box next to "RevaApplicationPlugin"
  9. Add the mcp to your agent harness:

    claude mcp add --scope user --transport http ReVa -- http://localhost:8080/mcp/message
    
    opencode mcp add ReVa --url http://localhost:8080/mcp/message
    

You can now use ReVa in your harness.