Contributing to NetSentinel¶
Thanks for your interest in contributing. This document covers everything from setting up a dev environment to shipping a release.
1. Prerequisites¶
| Requirement | Version | Notes |
|---|---|---|
| Python | 3.10 or newer (3.11 recommended for builds) | build.bat explicitly targets 3.11 to match GitHub Actions |
| pip | bundled with Python | |
| PyQt6 | ~6.11 | installed via requirements.txt |
| Npcap | latest | Windows only — required for packet-capture features (Rogue Bridge / STP, Broadcast Storm tabs). Download from npcap.com. Not needed to run the app or its other tabs. |
| Inno Setup | 6.x | Required only to build the Windows installer .exe. Download from jrsoftware.org. |
Optional system tools used at runtime but not required to develop:
nmap— OS fingerprinting and SYN scanner- Ookla Speedtest CLI (
winget install Ookla.Speedtest.CLI) — tier-1 speed test backend pywin32— Windows service build only (not needed for GUI or CLI)
2. Clone and set up¶
git clone https://github.com/ossianericson/netsentinel.git
cd netsentinel
pip install -r requirements.txt
2a. Platform setup {#platform-setup}¶
Windows¶
Install Npcap for packet-capture features (STP, storm detection, ARP monitor, DHCP detector). Run as Administrator for those pages.
If the installer is blocked on first run: right-click the downloaded file → Properties → check Unblock → OK, then run it. This does not apply to winget installs.
macOS¶
First launch requires right-click → Open to bypass Gatekeeper. Run with sudo python app.py to enable packet capture features. On Apple Silicon, ensure you are using a native arm64 Python build — x86_64 via Rosetta may have issues with Scapy and libpcap.
Linux (Ubuntu/Debian)¶
If the app fails to launch with a Qt platform plugin error:
The pinned dependencies in requirements.txt use ~= (compatible-release) specifiers — they allow only patch updates within the same major.minor, which prevents silent breaking changes. Do not loosen these pins without a deliberate review.
Core runtime dependencies installed by the above:
| Package | Purpose |
|---|---|
PyQt6~=6.11 |
UI framework |
scapy~=2.7 |
Packet capture (ARP, STP, storm analysis) |
matplotlib~=3.10 |
Charts and graphs |
psutil~=7.0 |
Active connections, process monitor |
speedtest-cli~=2.1 |
Fallback speed test backend |
keyring~=25.0 |
OS credential store (SMTP password, SNMP community, API keys) |
flask~=3.0 |
Local read-only REST API (127.0.0.1, disabled by default) |
cryptography~=44.0 |
Required by scapy; enables KRACK module |
maxminddb~=3.1 |
GeoLite2 IP geolocation |
3. Running the app from source¶
The app requires administrator/root privileges for features that use raw sockets (ARP scanning, STP detection, packet capture). On Windows, right-click your terminal and choose "Run as Administrator" before launching. On Linux/macOS:
For a quick import sanity check without opening the GUI:
This runs _smoke_test() in app.py, which imports every module and worker to catch missing dependencies or broken imports — the same check that build.bat runs before a PyInstaller build.
4. Running tests¶
CI / headless environments: PyQt6 requires a display. Set the platform to offscreen before running:
On Windows (PowerShell):
The conftest.py fixture at tests/conftest.py handles this automatically when tests are invoked via pytest — it creates a single session-scoped QApplication with ["-platform", "offscreen"] so Qt widget tests don't segfault between modules. If you set QT_QPA_PLATFORM=offscreen in the environment before running, that takes precedence.
After each test the fixture closes orphaned top-level widgets and runs three passes of processEvents() to drain deleteLater() queues. This prevents C-level segfaults caused by accumulated Qt objects across QWidget-heavy tests.
To run only the version consistency check (useful after a bump):
5. Building the installer¶
Windows¶
build.bat will:
- Create a
.venv311virtual environment using Python 3.11 (matches GitHub Actions). - Install all dependencies inside it.
- Run a pre-build smoke test (
python app.py --smoke). - Call
pyinstaller --clean -y NetSentinel.specto producedist\NetSentinel.exe. - Optionally build the CLI (
NetSentinelCLI.spec) and Windows service (NetSentinelSvc.spec). - Run a post-build smoke test against the bundled executable.
Selective builds:
build.bat --gui # GUI only (skip CLI and service)
build.bat --cli # CLI only
build.bat --debug # GUI with console window for diagnosing PyInstaller issues
After a successful GUI build, build the installer:
This produces dist\NetSentinel-Setup-X.Y.Z.exe.
macOS / Linux¶
Produces dist/NetSentinel (GUI) and optionally dist/NetSentinel-cli. The macOS build does not include Inno Setup packaging — distribute the raw executable or wrap it in a .app bundle manually.
6. Bumping the version¶
Never edit version strings by hand. Use the bump script every time — no exceptions (RULE-R1).
Example:
The script updates all 13 tracked locations atomically:
| File | What changes |
|---|---|
app.py |
setApplicationVersion("X.Y.Z") |
cli.py |
_VERSION = "X.Y.Z" |
apm.yml |
top-level version: X.Y.Z |
installer.iss |
#define MyAppVersion "X.Y.Z" |
build.bat |
version banner line |
build.sh |
version banner line |
README.md |
release badge link + What's New header |
tools/debug_launch.py |
setApplicationVersion("X.Y.Z") |
modules/rest_api.py |
"version" in /health endpoint |
.github/winget/NetSentinel.NetSentinel.yaml |
PackageVersion: X.Y.Z |
.github/winget/NetSentinel.NetSentinel.installer.yaml |
PackageVersion + installer URL |
.github/winget/NetSentinel.NetSentinel.locale.en-US.yaml |
PackageVersion: X.Y.Z |
After running, the script automatically executes tests/test_version_consistency.py and exits with code 1 if any location was missed.
Then commit and tag:
git add -p # review every change
git commit -m "chore: bump version to 1.60"
git tag v1.60
git push && git push --tags
7. Adding a new page¶
Pages live in ui/pages/. Follow these steps:
Step 1 — Create the page file
Your page class must be a QWidget subclass. Show meaningful content within 200 ms of navigation (RULE-UX1) — use MetricStore cached data for the initial render, then refresh via a worker signal.
Step 2 — Instantiate the page in _init_pages()
Open ui/dashboard.py and add the instantiation to _init_pages():
Step 3 — Register the page in _build_pro_nav()
Inside _build_pro_nav(), find the correct section block (see the nav section placement guide in .claude/rules/architecture.md) and call _nav_add_rail_item():
# Inside the correct _nav_begin_section("Monitor", "monitor") block:
self._nav_add_rail_item("◈ My Page", self._my_page)
The label string is the flyout item label — it must start with a geometric Unicode prefix character (see RULE-I4). Do not call _nav_add_page() — that method is legacy dead code.
For pages that require admin privileges, add the keyword argument:
For Security Audit section pages, also pass audit_item=True:
Step 4 — Update the smoke test
Add an import of your page to app.py _smoke_test() (RULE-T4).
Step 5 — Write tests
At minimum: one import test and one behavioural test in tests/test_my_page.py.
8. Adding a new module¶
Modules live in modules/. They contain pure business logic — no Qt imports, no widgets, no UI calls.
File size limit: No module file may exceed 780 lines (RULE-AH1). If you find yourself going over, extract cohesive sub-concerns into sibling modules.
Lazy imports for optional dependencies:
Any dependency that is not universally available (nmap, scapy, speedtest-cli, Npcap-dependent libs) must not be imported at module level (RULE-AH4). Wrap them in a lazy import inside the function that needs them:
def run_scan(target: str):
try:
import nmap
except ImportError:
return [] # degrade gracefully — never crash on import
# ... use nmap
The fallback must degrade gracefully: return empty results, emit an error signal, or show a banner. Never let an optional import crash the app.
Test file required (RULE-T1): Every new file added under modules/ must have a corresponding tests/test_<name>.py in the same PR. The test file must contain at minimum:
- one import test (verifies the module loads cleanly)
- one behavioural test covering the primary function or class
If you are adding a worker under workers/, it also needs a start/stop lifecycle test (RULE-T2): instantiate the worker, call start(), wait briefly, call stop(), and assert isRunning() is False.
9. Coding standards¶
Key rules from apm.yml that every contributor must follow:
No raw hex colours outside ui/styles.py and modules/colours.py (RULE-AH3 — blocking)
Running grep -rn '#[0-9a-fA-F]\{6\}' --include="*.py" and finding matches outside those two files is a CI failure. Add new colours to ui/styles.py (for UI/widget use) or modules/colours.py (for charts and HTML report use), then import from there.
No raw exceptions shown to users (RULE-A2 — required)
No Python traceback or AttributeError may ever appear in the UI. All worker error signals must be caught and translated to an actionable message using a helper like _friendly_error(exc) -> str in each page class. The message must say what failed, why it likely failed, and what the user should try next.
Severity labels must use the canonical four (RULE-A3 — required)
All severity/risk labels visible in the UI must be exactly one of: Info, Warning, High, Critical. Do not invent new strings (SEVERE, NOTICE, ALERT, etc.). The visual treatment is defined by RISK_COLORS / RISK_BG in ui/styles.py.
Every feature needs plain-English and technical views (RULE-A1 — required)
Scan results must be presented at two levels: a plain-English summary (what it means, what to do) visible by default, and full technical detail (raw values, MACs, exact RTTs, OIDs) accessible via a collapsible section or "Details" button.
New pages must show content within 200 ms (RULE-UX1 — required)
Use cached MetricStore data for the initial render, then refresh via a worker signal.
Every scan result table needs a right-click context menu (RULE-UX3 — required)
At minimum: "Copy" (selected row as text) and "How to Fix" (plain-English remediation panel). Use customContextMenuRequested signal — never override mousePressEvent.
Workers must not block for more than 5 seconds without a progress signal (RULE-AH2 — required)
Long-running workers must emit a progress(str) or status(str) signal at least every 5 seconds. Use msleep() in polling loops and check a _running flag on each iteration.
10. PR checklist¶
Before opening a pull request, confirm all of the following:
- [ ]
pytest tests/ -vpasses with zero failures (run withQT_QPA_PLATFORM=offscreenif headless) - [ ]
tests/test_version_consistency.pypasses — all 13 version locations are in sync - [ ] If a new module was added:
tests/test_<module>.pyexists with at least one import test and one behavioural test (RULE-T1) - [ ] If a new worker was added: start/stop lifecycle test exists (RULE-T2)
- [ ] If this is a bug fix: a regression test that fails before the fix and passes after is included (RULE-T3)
- [ ]
app.py _smoke_test()was updated if a new module or worker was added (RULE-T4) - [ ] No raw hex colour strings outside
ui/styles.py/modules/colours.py(grep '#[0-9a-fA-F]\{6\}'returns zero matches in other files) - [ ] No raw Python exceptions visible in the UI — all errors go through a
_friendly_error()translation layer - [ ] Manual smoke test:
python app.pylaunches without errors, the new page/feature is visible and functional - [ ] If a new page was added: it shows content within 200 ms (not a blank panel while a worker starts)
- [ ] PR title follows conventional commits style:
feat:,fix:,chore:,refactor:,test:,docs:
11. Release process¶
-
Bump the version — run
python bump_version.py X.Y.Zfrom the repo root. The script updates all 13 tracked locations and self-verifies by runningtests/test_version_consistency.py. -
Review and commit —
git add -pto review every diff, then: -
Tag and push —
-
CI builds the installer — the
release.ymlGitHub Actions workflow triggers on the new tag. It buildsNetSentinel-Setup-X.Y.Z.exeusing PyInstaller + Inno Setup, then attaches it to a GitHub Release. -
WinGet auto-submits — the workflow generates the three WinGet manifests (main, installer, locale) from the full template (RULE-R2) and opens a pull request against the
winget-pkgscommunity repo. Ookla.Speedtest.CLI is declared asExternalDependencies(notPackageDependencies) to avoid blocking installs (RULE-R3). -
Verify the release — once the GitHub Release is published, install from winget on a clean machine to confirm the installer works end-to-end: