~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:
- 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.
- Firmware-version variation breaks the version-banner regex. The marker
SB6141\s+Hardware\s+Versionmay 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. - 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).
- 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.logfor 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.getwithouttimeout=. A no-timeout call hangs forever on a misconfigured lab network. ALWAYS passtimeout=.
What would a reviewer ask?
- "Run your tool against an IP that responds with something other than an SB6141. What does it print? Is the message useful for diagnosis?"
- "If I change one byte in the response (e.g., the version banner now says
Hardware Version: 99), what happens? Should the tool care?" - "Why two markers rather than one? Why two rather than three? Defend the design."
Stretch (optional)
- Add a
--jsonoutput mode. Argparse flag; instead of human prose, emit{"match": true, "target": "...", "firmware": "..."}for machine consumption. - 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.
- 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.
- Refactor for testability. The current
fingerprint()function is tested viaunittest.mock.patchofrequests.get. Refactor to inject the HTTP-client function (dependency injection); test by passing a fake. Compare ergonomics.
Lab 4 v0.1.