Classroom Glossary Public page

Lab 4: Tool v0.1, Fingerprint Only

728 words

~3 hr. Build Tool v0.1: a Python CLI that probes the SB6141 admin interface and identifies it (or refuses). No destructive action; the entire tool is a non-destructive fingerprint probe.


Goal: ship sb6141_csrf/v01.py plus two unit tests plus a README stub. The tool: takes --target IP; issues one HTTP GET; checks the response for two SB6141 markers; prints a single-line result; exits with semantically-correct status code.

Estimated time: ~3 hr.

Prerequisites: Week 4 lecture. Labs 1-3 complete. Instructor counter-signature on the cyber-use authorization (Lab 3 gate).

Authorization line: Write in your lab notebook before any run against the live SB6141:

Lab 4 session, 2026-MM-DD HH:MM. Target SB6141 unit <serial> on isolated lab network;
scope per signed cohort authorization (filed 2026-MM-DD, counter-signed 2026-MM-DD);
Tool v0.1 fingerprint probe only; no state-changing action.

Setup

mkdir -p ~/adv-101/lab-4/sb6141_csrf
mkdir -p ~/adv-101/lab-4/tests
cd ~/adv-101/lab-4
touch sb6141_csrf/__init__.py

Confirm Python 3.11+ and requests, pytest:

python3 --version
python3 -m pip show requests | head -1
python3 -m pip show pytest | head -1

Part A: Write the fingerprint function (~45 min)

sb6141_csrf/v01.py:

"""ADV-101 Tool v0.1: SB6141 CSRF fingerprint-only probe.

Issues one HTTP GET to http://<target>/; checks the response for two
SB6141 markers (title + version banner). Exits with status 0 on
fingerprint match, status 3 on mismatch.

This version does NOT perform any destructive action. v0.2 adds the CSRF
reproduction gated behind --authorized-by and --dry-run; v0.3 adds
structured logging and idempotency. See SECURITY-MODEL.md.
"""

from __future__ import annotations

import argparse
import logging
import re
import sys

import requests

LOG = logging.getLogger('sb6141_csrf.v01')

TITLE_MARKER = re.compile(r'SB6141', re.IGNORECASE)
VERSION_MARKER = re.compile(r'SB6141\s+Hardware\s+Version', re.IGNORECASE)

EXIT_OK = 0
EXIT_RUNTIME_ERROR = 1
EXIT_USAGE_ERROR = 2
EXIT_FINGERPRINT_MISMATCH = 3


def fingerprint(target: str, timeout: float = 5.0) -> dict:
    """Probe http://<target>/ and decide whether it is an SB6141.

    Returns a dict with keys:
      match: bool, True if fingerprint matches
      reason: str, human-readable explanation
      firmware_hint: str | None, the version banner if extracted
    """
    url = f'http://{target}/'
    LOG.info('probing %s', url)
    try:
        resp = requests.get(url, timeout=timeout)
    except requests.exceptions.RequestException as e:
        LOG.error('request failed: %s', e)
        return {
            'match': False,
            'reason': f'request failed: {e}',
            'firmware_hint': None,
        }
    LOG.info('response received (HTTP %d, %d bytes)', resp.status_code, len(resp.text))
    title_hit = bool(TITLE_MARKER.search(resp.text))
    version_hit = bool(VERSION_MARKER.search(resp.text))
    if title_hit and version_hit:
        firmware = _extract_firmware(resp.text)
        return {
            'match': True,
            'reason': 'title + version banner present',
            'firmware_hint': firmware,
        }
    return {
        'match': False,
        'reason': f'title_hit={title_hit} version_hit={version_hit}',
        'firmware_hint': None,
    }


def _extract_firmware(html: str) -> str | None:
    """Pull the firmware version from the SB6141 admin page HTML, if present."""
    m = re.search(r'SB6141\s+Hardware\s+Version[:\s]+([0-9.]+)', html, re.IGNORECASE)
    if m:
        return m.group(1)
    return None


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog='sb6141-csrf-v01',
        description=(
            'SB6141 CSRF tool v0.1: fingerprint-only probe. '
            'Probes the target administrative interface and identifies it as an SB6141. '
            'NON-DESTRUCTIVE. v0.2 adds destructive-action capability gated behind '
            '--authorized-by and --dry-run.'
        ),
        epilog=(
            'Example:\n'
            '  %(prog)s --target 192.168.100.1\n'
            '\n'
            'Exit codes:\n'
            '  0  fingerprint matched (target is an SB6141)\n'
            '  1  runtime error (network unreachable; timeout)\n'
            '  2  usage error (missing/invalid argument)\n'
            '  3  fingerprint mismatch (target reachable but not SB6141)\n'
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        '--target',
        required=True,
        help='IP address or hostname of the SB6141 admin interface (e.g., 192.168.100.1)',
    )
    parser.add_argument(
        '--timeout',
        type=float,
        default=5.0,
        help='HTTP request timeout in seconds (default: %(default)s)',
    )
    parser.add_argument(
        '--verbose', '-v',
        action='store_true',
        help='enable DEBUG-level logging to stderr',
    )
    return parser


def main(argv: list[str] | None = None) -> int:
    args = build_parser().parse_args(argv)

    logging.basicConfig(
        level=logging.DEBUG if args.verbose else logging.INFO,
        format='%(asctime)s %(levelname)s %(name)s: %(message)s',
        stream=sys.stderr,
    )

    result = fingerprint(args.target, timeout=args.timeout)

    if result['match']:
        firmware = result['firmware_hint'] or 'unknown'
        print(f'SB6141 detected at {args.target}; firmware version {firmware}')
        return EXIT_OK

    if 'request failed' in result['reason']:
        print(f'Error: {result["reason"]}', file=sys.stderr)
        return EXIT_RUNTIME_ERROR

    print(
        f'Error: target at {args.target} does not fingerprint as SB6141 ({result["reason"]})',
        file=sys.stderr,
    )
    return EXIT_FINGERPRINT_MISMATCH


if __name__ == '__main__':
    sys.exit(main())

Part B: Test against the live SB6141 (~30 min)

After writing per-session authorization line in your lab notebook:

cd ~/adv-101/lab-4
python3 -m sb6141_csrf.v01 --target 192.168.100.1
# Expected: "SB6141 detected at 192.168.100.1; firmware version X.Y.Z"
# Exit code 0

python3 -m sb6141_csrf.v01 --target 192.168.100.99  # non-existent IP
# Expected: error message; exit code 1 (or 3 if a different device responds)

python3 -m sb6141_csrf.v01  # missing --target
# Expected: argparse usage error; exit code 2

Capture the outputs:

python3 -m sb6141_csrf.v01 --target 192.168.100.1 > run-sb6141.log 2>&1
echo "Exit code: $?" >> run-sb6141.log

If your specific SB6141 firmware uses different marker strings than the lecture defaults, adjust TITLE_MARKER and VERSION_MARKER in v01.py and re-test. Document the adjustment in your lab notebook.


Part C: Write unit tests (~45 min)

Tests live in tests/; they should pass WITHOUT a live SB6141 (using responses or unittest.mock to fake the HTTP layer).

tests/test_fingerprint.py:

"""Unit tests for sb6141_csrf.v01.fingerprint."""

from unittest.mock import patch, MagicMock

import pytest

from sb6141_csrf.v01 import fingerprint, _extract_firmware


def _make_mock_response(text: str, status: int = 200):
    mock = MagicMock()
    mock.status_code = status
    mock.text = text
    return mock


def test_fingerprint_matches_canonical_sb6141_html():
    """A well-formed SB6141 admin page fingerprints as a match."""
    html = (
        '<html><head><title>SB6141 Cable Modem</title></head>'
        '<body><span>SB6141 Hardware Version: 7.5.0</span></body></html>'
    )
    with patch('sb6141_csrf.v01.requests.get') as mock_get:
        mock_get.return_value = _make_mock_response(html)
        result = fingerprint('192.168.100.1')
    assert result['match'] is True
    assert result['firmware_hint'] == '7.5.0'


def test_fingerprint_rejects_non_sb6141():
    """A page without SB6141 markers fingerprints as no-match."""
    html = '<html><head><title>Apache test page</title></head></html>'
    with patch('sb6141_csrf.v01.requests.get') as mock_get:
        mock_get.return_value = _make_mock_response(html)
        result = fingerprint('192.168.100.1')
    assert result['match'] is False
    assert 'title_hit=False' in result['reason']


def test_fingerprint_rejects_title_only_match():
    """A page with title but no version banner fingerprints as no-match (two-marker discipline)."""
    html = '<html><head><title>SB6141 Cable Modem</title></head><body>(empty)</body></html>'
    with patch('sb6141_csrf.v01.requests.get') as mock_get:
        mock_get.return_value = _make_mock_response(html)
        result = fingerprint('192.168.100.1')
    assert result['match'] is False
    assert 'title_hit=True' in result['reason']
    assert 'version_hit=False' in result['reason']


def test_extract_firmware_pulls_version_string():
    """The firmware extractor returns the numeric version when the banner is present."""
    html = '<span>SB6141 Hardware Version: 7.5.0</span>'
    assert _extract_firmware(html) == '7.5.0'


def test_extract_firmware_returns_none_when_absent():
    """The firmware extractor returns None when the banner is missing."""
    html = '<html>(no banner)</html>'
    assert _extract_firmware(html) is None

Run the tests:

cd ~/adv-101/lab-4
python3 -m pytest tests/ -v
# All 5 tests should pass.

Part D: Write the README stub (~30 min)

README.md (project root for lab-4):

# sb6141-csrf-v01

ADV-101 lab tool, version 0.1: fingerprint-only SB6141 probe.

## Purpose

Identify whether an HTTP target at the given address is a Motorola SURFboard
SB6141 cable modem, based on two HTML markers (page title + version banner).
This is the foundation safety control for ADV-101's Tool v0.2 onward; v0.1
itself is non-destructive.

## Install

```bash
pip install requests

Usage

python3 -m sb6141_csrf.v01 --target 192.168.100.1

Successful output:

SB6141 detected at 192.168.100.1; firmware version 7.5.0

Exit codes:

Code Meaning
0 fingerprint matched
1 runtime error (network unreachable; timeout)
2 usage error
3 fingerprint mismatch

Tests

python3 -m pytest tests/ -v

Authorization

This tool is part of the ADV-101 cohort coursework. Use against any SB6141 you do not personally own and have written authorization to test is UNAUTHORIZED and may constitute a violation of the U.S. Computer Fraud and Abuse Act (18 U.S.C. §1030) or equivalent state-law statutes. See ~/adv-101/scope-limit.md for the cohort scope-limit document; see ~/adv-101/lab-portfolio.md for the per-session authorization log.

License

For coursework use only; not for redistribution outside the cohort without academy approval.

---

## Part E: Commit your work (~15 min)

```bash
cd ~/adv-101/lab-4
git add sb6141_csrf/__init__.py sb6141_csrf/v01.py tests/test_fingerprint.py README.md run-sb6141.log
git commit -m "Lab 4: Tool v0.1 fingerprint-only probe; 5 unit tests pass; live-SB6141 run captured"

Append the Lab 4 entry to your lab portfolio:

### Lab 4 session, 2026-MM-DD HH:MM

**Target:** SB6141 unit serial <X> on isolated lab network
**Action:** developed Tool v0.1 (fingerprint-only); tested against live SB6141 + mock
**Authorization basis:** per signed cohort authorization (counter-signed 2026-MM-DD)
**Session duration:** ~3h
**Artifacts produced:** `lab-4/sb6141_csrf/v01.py`; `lab-4/tests/test_fingerprint.py`;
`lab-4/README.md`; `lab-4/run-sb6141.log`
**Incidents:** none (fingerprint matched on first run; firmware version <X> recorded)
cd ~/adv-101
git add lab-portfolio.md
git commit -m "Portfolio: Lab 4 session entry"

Expected output / artifact

~/adv-101/lab-4/ containing:

  • sb6141_csrf/__init__.py (empty package marker)
  • sb6141_csrf/v01.py (the tool)
  • tests/test_fingerprint.py (5+ unit tests; all pass)
  • README.md (install + usage + exit codes + authorization disclaimer)
  • run-sb6141.log (captured live-run output)

All committed; portfolio entry added.


What's the failure mode?

This tool's likely failure modes:

  1. Tool runs against a non-SB6141 by accident. Exit code 3 catches this; the tool refuses to claim success. The point of v0.1 is that it CANNOT do harm if it misidentifies; the harm comes when v0.2's destructive actions run against the wrong target, and v0.2 inherits this fingerprint as its first gate.
  2. Firmware-version variation breaks the version-banner regex. The marker SB6141\s+Hardware\s+Version may not appear on every firmware. Defense: log the FULL response body at DEBUG (--verbose) so the operator can diagnose; offer to adjust the marker as part of cohort-specific tuning.
  3. A spoofed SB6141 page. An attacker who set up an HTTP server returning a SB6141-shaped page could trick the fingerprint. Two markers raise the bar; three markers raise it further. v0.1 is intentionally simple; the defense-in-depth is the operator's responsibility (use only the documented isolated lab network).
  4. Timeout too short. A laggy SB6141 on a slow lab network may take more than 5 seconds. Adjustable via --timeout; the default is fine for most lab setups.

Common pitfalls

  • Marker too specific or too loose. Too specific (matches only one firmware): the tool breaks on other units. Too loose (matches any cable-modem page): the tool false-matches. Pick markers that are SB6141-distinctive but not version-specific.
  • Skipping the live-run. The mock-based tests pass without a real SB6141; the live run is the real validation. Capture the live output to run-sb6141.log for the portfolio.
  • Forgetting --verbose. A --verbose (or -v) flag that toggles DEBUG-level stderr output is the practitioner-helpful affordance. Skipping it makes Lab 7's debugging harder.
  • requests.get without timeout=. A no-timeout call hangs forever on a misconfigured lab network. ALWAYS pass timeout=.

What would a reviewer ask?

  1. "Run your tool against an IP that responds with something other than an SB6141. What does it print? Is the message useful for diagnosis?"
  2. "If I change one byte in the response (e.g., the version banner now says Hardware Version: 99), what happens? Should the tool care?"
  3. "Why two markers rather than one? Why two rather than three? Defend the design."

Stretch (optional)

  1. Add a --json output mode. Argparse flag; instead of human prose, emit {"match": true, "target": "...", "firmware": "..."} for machine consumption.
  2. Try the tool against your mock SB6141 server (from Week 4 independent practice item 2). Confirm the tool fingerprints the mock correctly without requiring a physical modem.
  3. Add a third marker. Pick a third SB6141-distinctive marker (an embedded script src URL pattern; a specific image filename; an HTTP Server header). Require all three for match. Re-test.
  4. Refactor for testability. The current fingerprint() function is tested via unittest.mock.patch of requests.get. Refactor to inject the HTTP-client function (dependency injection); test by passing a fake. Compare ergonomics.

Lab 4 v0.1.