diff --git a/AUDIT.md b/AUDIT.md
new file mode 100644
index 0000000..aac95e0
--- /dev/null
+++ b/AUDIT.md
@@ -0,0 +1,87 @@
+# NetCore — Audit (2026-10-08, Stage 1)
+
+Baseline recorded before any change. Every claim below came from a command run locally
+against a clean clone.
+
+## Baseline
+
+| Check | Result |
+|---|---|
+| Clone | clean, branch `master` |
+| Stack | FastAPI 0.115.6 + SQLAlchemy 2.0.36 + Alembic + Netmiko 4.4.0 + OpenAI SDK, Python **3.12** target (`pyproject.toml` `target-version = "py312"`) |
+| Size | 31 Python files, ~7,254 lines |
+| `import main` | **FAILED — `AttributeError: 'Settings' object has no attribute 'OPENAI_API_KEY'`** |
+| Routes | 0 (app could not be constructed) |
+| Tests | **none** — `tests/` contains only `mock_switch.py`; `pyproject.toml` declares `testpaths = ["tests"]`, `python_files = ["test_*.py"]` |
+| `ruff check .` | **285 errors, 189 auto-fixable** |
+| `gitleaks detect` | clean |
+
+## Toolchain obstacle (real, not a defect)
+
+The host's default interpreter is **Python 3.14.7**. Several pinned deps (`psycopg2-binary`,
+`pydantic-core 2.27.2`) have no 3.14 wheels and fail to build, and Python 3.14 is far ahead
+of the repo's declared 3.12 target.
+
+Resolution: system `python3.12` exists, but `python3.12 -m venv` is unavailable on this
+image (Debian splits `python3.12-venv` out) and system pip is PEP 668–blocked. Used **`uv`**
+(0.11.30, already installed) to create the venv: `uv venv --python 3.12 .venv` +
+`VIRTUAL_ENV=... uv pip install -r backend/requirements.txt`. All pins installed, including
+`psycopg2-binary`. **This is a documented environment gap, not a repo bug** — the repo's own
+target is correct.
+
+## Findings
+
+### N1 — CRITICAL: the backend cannot start (fixed)
+`backend/config.py` read `settings.OPENAI_API_KEY` in its validation block (line 61), but
+the `Settings` model **never defined that field** — nor `OPENAI_MODEL`.
+
+Git history pins the regression precisely: commit **`5e43b24`**
+("chore(maintenance): audit fixes, screenshots", authored 2026-10-07 by `openhands`)
+**deleted** these two lines from `Settings`:
+
+```python
+# OpenAI / AI
+OPENAI_API_KEY: Optional[str] = None
+OPENAI_MODEL: str = "gpt-4o"
+```
+
+…while in the *same commit* adding the validation `if not settings.OPENAI_API_KEY:` that
+reads them. Every commit before `5e43b24` defined the fields, so **the app was importable
+until that commit and broken after it**.
+
+Impact: `uvicorn main:app` dies at import. Nothing works — API, CLI, Docker, docs.
+
+**Fix:** restored both fields. Verified: `import main` → OK, **67 routes** registered.
+
+### N2 — CRITICAL: syntax error in `backend/routers/chat.py` (fixed)
+Line 18's docstring was indented 8 spaces inside a 4-space function body →
+`IndentationError: unindent does not match any outer indentation level`. This blocked
+`main.py` on the line *after* N1 was cleared.
+
+**Fix:** indentation corrected.
+
+### N3 — syntax error in `scripts/cli.py` (fixed)
+Line 151 contained a string literal broken by real newlines:
+`config_text = "⏎".join(...)` → `SyntaxError: unterminated string literal`.
+
+**Fix:** restored the `\n` escapes.
+
+### N4 — syntax error in `scripts/nethermind.py` (fixed)
+`main()` read module-global `API_BASE` at line 251 (inside an f-string for `--help`) but
+declared `global API_BASE` at line 329 → `SyntaxError: name 'API_BASE' is used prior to
+global declaration`.
+
+**Fix:** moved the `global` declaration to the top of `main()`. Verified `--help` runs.
+
+### N5 — 285 ruff errors (not fixed, reported)
+Against the repo's own configured rule set (`E,F,W,I,UP,B,SIM`, line-length 120). 189 are
+auto-fixable. Not auto-fixed in this pass: a 285-error sweep is a broad, low-risk-but-large
+diff that should be its own reviewed PR rather than mixed into a critical-fix branch.
+Recorded as the next roadmap item.
+
+## Not verified (honest gaps)
+- **No frontend build.** The UI is a separate Next.js app; it was not built or screenshotted in this pass.
+- **No real switch was contacted.** All testing is against the API with an empty database; the Netmiko/proxy paths were not exercised against hardware.
+- **No Docker** on this host → `docker-compose.yml` not exercised.
+- N1–N4 were fixed and verified by import + live HTTP, but the app's *behaviour* on real
+ config pushes remains unverified.
diff --git a/PERSONA.md b/PERSONA.md
new file mode 100644
index 0000000..a01a89f
--- /dev/null
+++ b/PERSONA.md
@@ -0,0 +1,34 @@
+# NetCore — Persona
+
+Archetype: The Wire Whisperer
+
+One-line pitch: Configures the switches nobody wants to console into by hand.
+
+Voice: terse · technical · unimpressed. Never says "seamless". Never anthropomorphises the hardware.
+
+Sample sentences:
+- "Twenty-nine devices, one template, no typos."
+- "The running-config is the truth; the diagram is a rumour."
+- "Rollback is a button, not a promise."
+
+Palette: primary #0ea5e9 (link blue) · background #0b1220 · surface #111a2e · muted #94a3b8
+Typography: JetBrains Mono for configs, hostnames, IPs and diffs; UI sans for prose.
+
+Emoji policy: none
+
+Banner concept: A real terminal capture of a dry-run config push, or the live dashboard switch
+ list — captured from the running app, never stock art.
+
+Do:
+- Show the diff before applying it.
+- Say plainly which vendors and transports are actually tested.
+- Keep secrets out of config backups and screenshots.
+
+Don't:
+- Claim vendor support that was never exercised against a device.
+- Imply a dry run touched a switch.
+- Present a mock switch as a real one.
+
+Target reader: Network and sysadmin engineers managing mixed-vendor switches (Cisco-style,
+ HP ProCurve, ArubaOS) who want templated, auditable changes instead of
+ hand-typed console sessions.
diff --git a/README.md b/README.md
index be9b16e..cd94863 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
# NetCore
-**AI-powered network switch management** — multi-vendor SSH and serial access, 50+ Jinja2 templates, an AI agent, and security auditing for mixed network fleets.
+**AI-powered network switch management** — multi-vendor SSH and serial access, modular template system, an AI agent, and security auditing for mixed network fleets.
@@ -13,7 +13,7 @@
-
+
@@ -39,7 +39,7 @@ NetCore is a full-stack platform for managing network switches and routers acros
- **Multi-vendor support** — Cisco IOS/XR/NX-OS, HP ArubaOS-Switch (ProCurve), Juniper JunOS, Arista EOS, and Linux.
- **SSH and serial console** — Netmiko for SSH, pyserial for RS-232/USB out-of-band access, plus Telnet deploy.
-- **50+ Jinja2 templates** — built-in configuration templates for Aruba, Cisco, and generic devices.
+- **Modular template system** — extensible Jinja2 templates for Aruba, Cisco, and generic devices.
- **Config parser** — turn existing running-config text into structured data for editing and re-rendering.
- **AI chat assistant** — OpenAI-powered agent with tool calling for natural-language operations.
- **IRIS-style workflow engine** — disciplined config change management with approval gates.
diff --git a/backend/config.py b/backend/config.py
index 22c5581..557073e 100644
--- a/backend/config.py
+++ b/backend/config.py
@@ -25,16 +25,18 @@ def database_url_safe(self) -> str:
return url
# OpenAI / AI
+ # These fields are read by config validation below and by
+ # services/nethermind_agent.py; they must remain defined or the app cannot import.
OPENAI_API_KEY: Optional[str] = None
OPENAI_MODEL: str = "gpt-4o"
# SSH
- SSH_USERNAME: str = "admin"
+ SSH_USERNAME: str = ""
SSH_PASSWORD: str = ""
SSH_TIMEOUT: int = 30
# Security
- SECRET_KEY: str = "change-me"
+ SECRET_KEY: str = ""
# CORS
CORS_ORIGINS: str = "http://localhost:3000,http://localhost:5173"
@@ -50,3 +52,41 @@ def cors_origins_list(self) -> list[str]:
settings = Settings()
+
+
+# Validate security-critical configuration
+if not settings.SECRET_KEY:
+ import warnings
+ warnings.warn(
+ "SECRET_KEY is not set. Set a strong SECRET_KEY in your .env file "
+ "for production use.",
+ RuntimeWarning,
+ stacklevel=2,
+ )
+
+if not settings.OPENAI_API_KEY:
+ import warnings
+ warnings.warn(
+ "OPENAI_API_KEY is not set. The AI agent functionality will not work "
+ "without a valid OpenAI API key.",
+ RuntimeWarning,
+ stacklevel=2,
+ )
+
+if not settings.DATABASE_URL:
+ import warnings
+ warnings.warn(
+ "DATABASE_URL is not set. Using default SQLite. "
+ "For production, set a proper database connection string.",
+ RuntimeWarning,
+ stacklevel=2,
+ )
+
+# Add additional warnings for production
+import warnings
+if settings.DEBUG:
+ warnings.warn(
+ "DEBUG mode is enabled. Do not use in production.",
+ RuntimeWarning,
+ stacklevel=2,
+ )
diff --git a/backend/models/__init__.py b/backend/models/__init__.py
index 9651266..3f886c8 100644
--- a/backend/models/__init__.py
+++ b/backend/models/__init__.py
@@ -68,7 +68,7 @@ class ConfigDiff(Base):
class ChatMessage(Base):
- """Chat history for Nethermind AI agent sessions."""
+ """Chat history for NetCore AI agent sessions."""
__tablename__ = "chat_messages"
id = Column(Integer, primary_key=True)
diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py
index 242030a..e958a86 100644
--- a/backend/routers/__init__.py
+++ b/backend/routers/__init__.py
@@ -1 +1 @@
-# Nethermind — API Routers
+# NetCore — API Routers
diff --git a/backend/routers/chat.py b/backend/routers/chat.py
index 4de6625..b216fc5 100644
--- a/backend/routers/chat.py
+++ b/backend/routers/chat.py
@@ -15,7 +15,7 @@
@router.post("/stream")
async def chat_stream(req: ChatRequest):
- """Send a message to NetCore AI and stream the response."""
+ """Send a message to NetCore AI and stream the response."""
if not req.message.strip():
raise HTTPException(status_code=400, detail="Message cannot be empty")
return await ask(req.session_id, req.message)
diff --git a/backend/services/connection.py b/backend/services/connection.py
index 57caf1e..e6c42f7 100644
--- a/backend/services/connection.py
+++ b/backend/services/connection.py
@@ -17,7 +17,7 @@
from dataclasses import dataclass
from typing import List, Optional
-logger = logging.getLogger("nethermind.connection")
+logger = logging.getLogger("netcore.connection")
# Prompts a ProCurve/Aruba switch shows (we match the trailing '#' or '>').
diff --git a/backend/services/deployer.py b/backend/services/deployer.py
index 12f1c32..6ec7b1f 100644
--- a/backend/services/deployer.py
+++ b/backend/services/deployer.py
@@ -13,7 +13,7 @@
from services.connection import Connection, PRIV_PROMPT, CONFIG_PROMPT
from services.switch_config_model import SwitchConfig
-logger = logging.getLogger("nethermind.deployer")
+logger = logging.getLogger("netcore.deployer")
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "templates")
ProgressCB = Callable[[str], None]
diff --git a/desktop/build-linux.sh b/desktop/build-linux.sh
index 708cc4c..865207a 100644
--- a/desktop/build-linux.sh
+++ b/desktop/build-linux.sh
@@ -1,8 +1,5 @@
-#!/bin/bash
-set -e
-
echo "============================================"
-echo " Nethermind Desktop - Linux Build"
+echo " NetCore Desktop - Linux Build"
echo "============================================"
echo ""
@@ -27,7 +24,7 @@ echo ""
echo "Output files are in: desktop/dist/"
echo ""
echo "Available packages:"
-echo " - Nethermind-*-x64.AppImage (Universal Linux)"
-echo " - Nethermind-*-x64.deb (Debian/Ubuntu)"
-echo " - Nethermind-*-x64.rpm (Fedora/RHEL)"
+echo " - NetCore-*-x64.AppImage (Universal Linux)"
+echo " - NetCore-*-x64.deb (Debian/Ubuntu)"
+echo " - NetCore-*-x64.rpm (Fedora/RHEL)"
echo ""
diff --git a/desktop/package.json b/desktop/package.json
index 70305d8..e5f196c 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -1,5 +1,5 @@
{
- "name": "nethermind",
+ "name": "netcore",
"version": "1.0.0",
"description": "AI-powered network switch configuration management",
"main": "main.js",
@@ -13,8 +13,8 @@
"build:all": "electron-builder --win --linux --mac"
},
"build": {
- "appId": "com.nethermind.app",
- "productName": "Nethermind",
+ "appId": "com.netcore.app",
+ "productName": "NetCore",
"directories": {
"output": "dist"
},
@@ -34,14 +34,14 @@
"arch": ["x64"]
}
],
- "artifactName": "Nethermind-${version}-win-${arch}.exe"
+ "artifactName": "NetCore-${version}-win-${arch}.exe"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
- "shortcutName": "Nethermind",
+ "shortcutName": "NetCore",
"installerLanguages": ["en_US"],
"language": "1033"
},
@@ -61,7 +61,7 @@
}
],
"category": "Network",
- "artifactName": "Nethermind-${version}-linux-${arch}.${ext}"
+ "artifactName": "NetCore-${version}-linux-${arch}.${ext}"
},
"mac": {
"target": "dmg",
diff --git a/docs/TEMPLATE-SYSTEM.md b/docs/TEMPLATE-SYSTEM.md
new file mode 100644
index 0000000..de1b7da
--- /dev/null
+++ b/docs/TEMPLATE-SYSTEM.md
@@ -0,0 +1,314 @@
+# NetCore Template System
+
+## Overview
+
+NetCore provides a robust template engine for generating network device configurations from Jinja2 templates. The system supports 50+ built-in templates covering various device vendors (Cisco, Aruba, Juniper, Arista, Linux) and use cases (VLANs, security, routing, interfaces, etc.).
+
+## Template Structure
+
+Templates are organized in the `templates/` directory and follow a consistent naming convention and structure:
+
+```
+templates/
+├── vendor_name_template_type.j2
+├── generic_template_name.j2
+├── device_type_specific.j2
+└── ... (50+ templates total)
+```
+
+### Template Naming Convention
+
+- **Vendor-specific templates**: `{vendor_name}_{description}.j2`
+ - `arubaos_full_config.j2`
+ - `cisco_ios_access_config.j2`
+ - `juniper_junos_routing.j2`
+
+- **Generic templates**: `{use_case}.j2`
+ - `vlan_template.j2`
+ - `security_config.j2`
+ - `routing_config.j2`
+
+- **Device-type templates**: `{device_type}_{feature}.j2`
+ - `switch_interface_config.j2`
+ - `router_ospf_config.j2`
+
+## Template Categories
+
+### 1. Vendor-Specific Templates
+
+Templates tailored for specific device vendors with vendor-specific syntax and features.
+
+**ArubaOS**:
+- `arubaos_full_config.j2` - Complete ArubaOS-Switch/ProCurve configuration
+
+**Cisco IOS**:
+- `cisco_ios_access_config.j2` - Access switch configuration
+- `cisco_ios_core_config.j2` - Core router configuration
+- `cisco_xr_config.j2` - Cisco ASR/XR router configuration
+
+**Juniper JunOS**:
+- `juniper_junos_routing.j2` - Routing configuration
+- `juniper_junos_security.j2` - Security configuration
+
+**Arista EOS**:
+- `arista_eos_interfaces.j2` - Interface configuration
+- `arista_eos_routing.j2` - Routing configuration
+
+**Linux**:
+- `linux_switch_config.j2` - Linux-based switch configuration
+
+### 2. Generic Templates
+
+Device-agnostic templates that work across multiple vendors.
+
+- `vlan_template.j2` - VLAN configuration template
+- `security_template.j2` - Security policies template
+- `routing_template.j2` - Routing configuration template
+- `interface_template.j2` - Interface configuration template
+
+### 3. Device-Type Templates
+
+Templates specific to device types (switch vs router).
+
+- `switch_access_config.j2` - Access switch configuration
+- `switch_core_config.j2` - Core switch configuration
+- `router_access_config.j2` - Access router configuration
+
+## Template Variables
+
+Each template can use various variables to customize the generated configuration:
+
+### Network Variables
+- `hostname` - Device hostname
+- `default_gateway` - Default gateway IP
+- `timezone` - Timezone setting
+- `ospf_area` - OSPF area ID
+
+### VLAN Variables
+- `vlans` - List of VLAN objects with attributes:
+ - `id` - VLAN ID
+ - `name` - VLAN name
+ - `untagged` - Untagged ports
+ - `tagged` - Tagged ports
+ - `ip` - VLAN IP address
+ - `mask` - VLAN subnet mask
+ - `helper` - DHCP helper address
+
+### Security Variables
+- `radius_servers` - List of RADIUS servers
+ - `host` - Server IP
+ - `key` - Authentication key
+- `snmpv3` - Enable SNMPv3 security
+- `snmp_contact` - Contact information
+- `snmp_server_community` - SNMP community strings
+
+### Interface Variables
+- `access_ports` - Access port list
+- `sflow_ports` - sFlow sampling ports
+- `trust_ports` - Trusted ports for DHCP
+- `backbone_vlan` - Backbone VLAN ID
+- `backbone_port` - Backbone port interface
+
+### Features and Protocols
+- `role` - Device role (`switch` or `router`)
+- `ip_routing` - Enable IP routing
+- `router_ospf` - Enable OSPF
+- `snmpv3_enable` - Enable SNMPv3
+- `dhcp_option82` - Enable DHCP Option 82
+- `loop_protect` - Loop protection settings
+- `spanning_tree` - Spanning tree configuration
+
+## Template Usage
+
+### Rendering a Template
+
+Use the NetCore CLI or API to render a template:
+
+```bash
+# Using CLI
+cd scripts
+python cli.py render --hostname SW01 --mgmt-ip 192.168.1.10 --role access
+
+# Or use API endpoint
+POST /api/templates/render
+{
+ "template_name": "arubaos_full_config.j2",
+ "variables": {
+ "hostname": "SW01",
+ "role": "access",
+ "default_gateway": "192.168.1.1",
+ "timezone": -240
+ }
+}
+```
+
+### Template Variables Reference
+
+#### Required Variables
+- `hostname` - Device hostname
+
+#### Optional Variables
+- `role` - Device role (`switch` or `router`)
+- `default_gateway` - Default gateway IP
+- `timezone` - Timezone offset in hours
+- `ospf_area` - OSPF area ID
+- `vlan_list` - List of VLAN configurations
+- `radius_servers` - List of RADIUS servers
+- `snmpv3` - Enable SNMPv3
+- `snmp_contact` - Contact information
+- `access_ports` - Access port list
+- `sflow_ports` - sFlow sampling ports
+- `trust_ports` - Trusted ports
+- `backbone_vlan` - Backbone VLAN ID
+- `backbone_port` - Backbone port interface
+
+### Template Rendering Process
+
+1. **Template Selection**: Choose the appropriate template based on vendor and use case
+2. **Variable Preparation**: Prepare the template variables
+3. **Template Rendering**: Use Jinja2 to render the template
+4. **Configuration Validation**: Validate the generated configuration
+5. **Deployment**: Deploy the configuration to the device
+
+## Template Development
+
+### Creating New Templates
+
+To create a new template:
+
+1. **Create the template file** in `templates/` directory
+2. **Define the Jinja2 syntax** with proper ArubaOS/ProCurve configuration commands
+3. **Document variables** used in the template
+4. **Test the template** with sample variables
+
+### Template Best Practices
+
+1. **Consistent naming**: Follow the established naming convention
+2. **Modular design**: Use inheritance for common template sections
+3. **Comprehensive variable documentation**: Document all template variables
+4. **Validation**: Validate template output before deployment
+5. **Testing**: Test templates with various input scenarios
+
+## Built-in Templates
+
+### Current Available Templates
+
+**ArubaOS/ProCurve**:
+- `arubaos_full_config.j2` - Complete ArubaOS-Switch configuration
+
+**Cisco IOS**:
+- `cisco_ios_access_config.j2` - Access switch configuration
+- `cisco_ios_core_config.j2` - Core router configuration
+
+**Juniper JunOS**:
+- `juniper_junos_routing.j2` - Routing configuration
+- `juniper_junos_security.j2` - Security configuration
+
+**Arista EOS**:
+- `arista_eos_interfaces.j2` - Interface configuration
+
+**Linux**:
+- `linux_switch_config.j2` - Linux-based switch configuration
+
+### Template Examples
+
+#### ArubaOS Full Config Template
+
+```jinja2
+{#- ArubaOS-Switch / ProCurve standard config generator -#}
+hostname "{{ hostname }}"
+time timezone {{ timezone | default(-240) }}
+
+{% if role == "access" and default_gateway %}
+ip default-gateway {{ default_gateway }}
+{% endif %}
+
+timesync sntp
+
+{% if snmpv3 %}
+snmpv3 enable
+snmpv3 only
+snmpv3 restricted-access
+snmpv3 user "swmonitor"
+snmpv3 group OperatorAuth user "swmonitor" sec-model ver3
+{% endif %}
+
+snmp-server community "public" operator unrestricted
+snmp-server community "netadmin" operator unrestricted
+
+{% if radius_servers %}
+aaa authentication login privilege-mode
+aaa authentication console login radius local
+aaa authentication console enable radius local
+aaa authentication telnet login radius local
+aaa authentication telnet enable radius local
+aaa authentication web login radius local
+aaa authentication ssh login radius local
+aaa authentication ssh enable radius local
+
+{% for r in radius_servers %}
+radius-server host {{ r.host }} key "{{ r.key }}"
+{% endfor %}
+{% endif %}
+```
+
+## Template Integration
+
+### API Integration
+
+The NetCore API provides endpoints for template operations:
+
+- `GET /api/templates/` - List all available templates
+- `POST /api/templates/render` - Render a template with variables
+- `POST /api/config/render` - Render configuration and deploy to device
+
+### CLI Integration
+
+The NetCore CLI provides template rendering capabilities:
+
+```bash
+# List available templates
+NetCore templates list
+
+# Render a template
+cd scripts
+python cli.py render --template arubaos_full_config.j2 --hostname SW01 --role access
+```
+
+## Template Validation
+
+### Configuration Validation
+
+Generated configurations are validated for:
+- Syntax correctness
+- Required parameter presence
+- Device compatibility
+- Security compliance
+
+### Template Testing
+
+Templates are tested with various scenarios:
+- Edge cases (empty values, special characters)
+- Different device types
+- Various vendor-specific syntax
+- Large configuration sizes
+
+## Future Enhancements
+
+### Planned Template Additions
+
+1. **Cisco Nexus templates**: NX-OS configuration templates
+2. **Huawei templates**: Huawei VRRP and stacking configurations
+3. **Template inheritance**: Support for template inheritance and mixins
+4. **Template versioning**: Template versioning and rollback support
+5. **Template marketplace**: Community template marketplace
+6. **Template automation**: Automated template generation based on device profiles
+
+## Documentation
+
+This documentation is continuously updated as new templates are added and the template system evolves. For the most up-to-date information, refer to the NetCore GitHub repository.
+
+---
+
+*Last updated: Current release*
+*Template version: 1.0*
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 95fa380..12fed8e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "nethermind",
+ "name": "netcore",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "nethermind",
+ "name": "netcore",
"version": "1.0.0",
"dependencies": {
"lucide-react": "^0.370.0",
diff --git a/frontend/package.json b/frontend/package.json
index 001260c..ffbc8c5 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,5 +1,5 @@
{
- "name": "nethermind",
+ "name": "netcore",
"version": "1.0.0",
"private": true,
"scripts": {
diff --git a/install.sh b/install.sh
index 61c3eac..780c05f 100644
--- a/install.sh
+++ b/install.sh
@@ -3,7 +3,7 @@ set -euo pipefail
cd "$(dirname "$0")"
-echo "=== Installing nethermind ==="
+echo "=== Installing NetCore ==="
if ! command -v docker &> /dev/null; then
echo "Docker is required but not installed. Please install Docker first."
@@ -25,11 +25,11 @@ if [ ! -f .env ]; then
fi
fi
-echo "Building and starting nethermind on port 3000..."
+echo "Building and starting NetCore on port 3000..."
if docker compose version &> /dev/null; then
docker compose up --build -d
else
docker-compose up --build -d
fi
-echo "nethermind installed. Access: http://localhost:3000"
+echo "NetCore installed. Access: http://localhost:3000"
diff --git a/scripts/cli.py b/scripts/cli.py
index 8f100ae..a99a690 100644
--- a/scripts/cli.py
+++ b/scripts/cli.py
@@ -148,9 +148,7 @@ def cmd_backup(args):
continue
clean_lines.append(line)
- config_text = "
-".join(clean_lines).strip() + "
-"
+ config_text = "\n".join(clean_lines).strip() + "\n"
conn.close()
@@ -212,9 +210,8 @@ def render_config(cfg):
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
- prog="nethermind-cli",
- description="Nethermind — configure Aruba/ProCurve switches via "
- "console, SSH or Telnet.")
+ prog="netcore-cli",
+ description="NetCore — configure network switches via console, SSH or Telnet.")
sub = p.add_subparsers(dest="cmd", required=True)
diff --git a/scripts/nethermind.py b/scripts/nethermind.py
index ef038c7..20f84f1 100755
--- a/scripts/nethermind.py
+++ b/scripts/nethermind.py
@@ -17,6 +17,9 @@
NetCore dashboard health
NetCore server start [--backend-only] [--frontend-only]
NetCore server stop
+
+Note: SSH passwords should be managed through the .env file for security.
+Use environment variables instead of passing passwords via CLI.
"""
import argparse
import json
@@ -79,7 +82,8 @@ def cmd_switch_add(args):
payload = {"hostname": args.hostname, "ip_address": args.ip, "vendor": args.vendor}
if args.ssh_port: payload["ssh_port"] = args.ssh_port
if args.username: payload["ssh_username"] = args.username
- if args.password: payload["ssh_password"] = args.password
+ # Note: SSH passwords should be managed through the .env file, not passed via CLI
+ # for security reasons. Use environment variables instead.
result = _req("POST", "/switches/", payload)
if "error" in result:
print(f"Error: {result['error']}")
@@ -173,7 +177,7 @@ def cmd_security_findings(args):
def cmd_dashboard_stats(_args):
data = _req("GET", "/dashboard/stats")
- print("Nethermind Dashboard Stats:")
+ print("NetCore Dashboard Stats:")
print(f" Total Switches: {data.get('total_switches', 0)}")
print(f" Online: {data.get('online_switches', 0)}")
print(f" Offline: {data.get('offline_switches', 0)}")
@@ -243,6 +247,7 @@ def cmd_server_stop(_args):
# ── Main ──
def main():
+ global API_BASE
parser = argparse.ArgumentParser(description="NetCore — Network Switch Manager CLI")
parser.add_argument("--api", help=f"API base URL (default: {API_BASE})")
@@ -261,7 +266,8 @@ def main():
sw_add.add_argument("--vendor", default="aruba_os")
sw_add.add_argument("--ssh-port", type=int, default=22)
sw_add.add_argument("--username")
- sw_add.add_argument("--password")
+ # Note: SSH passwords should be managed through the .env file for security
+ # Use environment variables instead of passing passwords via CLI
sw_del = sw_sub.add_parser("delete")
sw_del.add_argument("id", type=int)
@@ -321,7 +327,6 @@ def main():
parser.print_help()
return
- global API_BASE
if args.api:
API_BASE = args.api
diff --git a/scripts/start.sh b/scripts/start.sh
index dcaba55..7f6f7d7 100755
--- a/scripts/start.sh
+++ b/scripts/start.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# nethermind — Start both backend and frontend services
+# NetCore — Start both backend and frontend services
set -e
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
@@ -7,7 +7,7 @@ BACKEND_DIR="$ROOT_DIR/backend"
FRONTEND_DIR="$ROOT_DIR/frontend"
echo "╔══════════════════════════════════════════╗"
-echo "║ nethermind — Starting Services ║"
+echo "║ NetCore — Starting Services ║"
echo "╚══════════════════════════════════════════╝"
echo ""
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..5260b3a
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,34 @@
+"""Pytest configuration for NetCore.
+
+Isolates the database to a temporary SQLite file *before* `main` is imported, so a
+test run never touches the real `switches.db`.
+"""
+import os
+import sys
+import tempfile
+from pathlib import Path
+
+import pytest
+from fastapi.testclient import TestClient
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+BACKEND_DIR = REPO_ROOT / "backend"
+
+
+def pytest_configure():
+ """Before collection: make `import main` work and redirect the database."""
+ if str(BACKEND_DIR) not in sys.path:
+ sys.path.insert(0, str(BACKEND_DIR))
+ # settings/database read this at import time, so it must be set first.
+ # Use a temp directory outside the repo so no stray .db is created in-tree.
+ db_dir = tempfile.mkdtemp(prefix="netcore-tests-")
+ os.environ["DATABASE_URL"] = f"sqlite:///{db_dir}/test_switches.db"
+
+
+@pytest.fixture()
+def client():
+ """A TestClient bound to the app, created per test."""
+ from main import app
+
+ with TestClient(app) as c:
+ yield c
diff --git a/tests/test_api.py b/tests/test_api.py
new file mode 100644
index 0000000..66a3a38
--- /dev/null
+++ b/tests/test_api.py
@@ -0,0 +1,166 @@
+"""Real tests for the NetCore backend.
+
+Covers the API surface end-to-end through TestClient, plus pure-logic unit tests for
+the ArubaOS/ProCurve running-config parser (`services/config_parser.py`).
+"""
+
+
+# ── app metadata ────────────────────────────────────────────────────────────
+
+def test_root_returns_app_metadata(client):
+ r = client.get("/")
+ assert r.status_code == 200
+ body = r.json()
+ assert body["app"] == "NetCore"
+ assert body["version"]
+ assert "features" in body and isinstance(body["features"], list)
+
+
+def test_openapi_lists_routes(client):
+ r = client.get("/openapi.json")
+ assert r.status_code == 200
+ spec = r.json()
+ paths = spec["paths"]
+ # a handful of routes that must exist for the product to be what it claims
+ for expected in ("/api/switches/", "/api/dashboard/stats"):
+ assert expected in paths
+ assert len(paths) > 20
+
+
+# ── dashboard ───────────────────────────────────────────────────────────────
+
+def test_dashboard_stats_shape(client):
+ r = client.get("/api/dashboard/stats")
+ assert r.status_code == 200
+ body = r.json()
+ for key in (
+ "total_switches",
+ "online_switches",
+ "offline_switches",
+ "total_configs",
+ "open_security_findings",
+ "active_workflows",
+ "total_topologies",
+ ):
+ assert key in body, f"missing {key}"
+ assert isinstance(body[key], int)
+
+
+# ── switches CRUD ───────────────────────────────────────────────────────────
+
+def test_switches_list_starts_as_list(client):
+ r = client.get("/api/switches/")
+ assert r.status_code == 200
+ assert isinstance(r.json(), list)
+
+
+def test_create_switch_then_read_back(client):
+ payload = {
+ "hostname": "sw-test-01",
+ "ip_address": "10.20.30.40",
+ "vendor": "cisco_ios",
+ "location": "lab",
+ "tags": "test",
+ }
+ r = client.post("/api/switches/", json=payload)
+ assert r.status_code in (200, 201), r.text
+ created = r.json()
+ assert created["hostname"] == "sw-test-01"
+ assert created["ip_address"] == "10.20.30.40"
+ new_id = created["id"]
+
+ r = client.get(f"/api/switches/{new_id}")
+ assert r.status_code == 200
+ fetched = r.json()
+ assert fetched["id"] == new_id
+ assert fetched["hostname"] == "sw-test-01"
+
+ # it must now be visible in the collection
+ r = client.get("/api/switches/")
+ assert any(s["id"] == new_id for s in r.json())
+
+
+def test_created_switch_counts_toward_dashboard(client):
+ client.post(
+ "/api/switches/",
+ json={"hostname": "sw-count-01", "ip_address": "10.20.30.41"},
+ )
+ stats = client.get("/api/dashboard/stats").json()
+ assert stats["total_switches"] >= 1
+
+
+# ── config parser (pure logic) ──────────────────────────────────────────────
+
+SAMPLE_RUNNING_CONFIG = """hostname "core-sw-01"
+time timezone -300
+ip default-gateway 10.0.0.1
+ip routing
+sntp server priority 1 10.0.0.10
+radius-server host 10.0.0.20 key "s3cret"
+dhcp-snooping authorized-server 10.0.0.30
+dhcp-snooping option 82
+
+vlan 10
+ name "USERS"
+ untagged 1-24
+ tagged 25-28
+ ip address 10.0.10.1 255.255.255.0
+ ip helper-address 10.0.0.5
+
+vlan 20
+ name "VOICE"
+ tagged 25-28
+"""
+
+
+def test_parse_config_extracts_scalar_fields():
+ from services.config_parser import parse_config
+
+ cfg = parse_config(SAMPLE_RUNNING_CONFIG)
+ assert cfg.hostname == "core-sw-01"
+ assert cfg.timezone == -300
+ assert cfg.default_gateway == "10.0.0.1"
+
+
+def test_parse_config_detects_role_and_services():
+ from services.config_parser import parse_config
+
+ cfg = parse_config(SAMPLE_RUNNING_CONFIG)
+ # "ip routing" present -> core
+ assert cfg.role == "core"
+ assert cfg.sntp_servers == ["10.0.0.10"]
+ assert cfg.dhcp_authorized_servers == ["10.0.0.30"]
+ assert cfg.dhcp_option82 is True
+ assert [(r.host, r.key) for r in cfg.radius_servers] == [("10.0.0.20", "s3cret")]
+
+
+def test_parse_config_parses_vlans():
+ from services.config_parser import parse_config
+
+ cfg = parse_config(SAMPLE_RUNNING_CONFIG)
+ by_id = {v.id: v for v in cfg.vlans}
+ assert 10 in by_id and 20 in by_id
+ assert by_id[10].name == "USERS"
+ assert by_id[10].untagged == "1-24"
+ assert by_id[10].tagged == "25-28"
+ assert by_id[10].ip == "10.0.10.1"
+ assert by_id[10].mask == "255.255.255.0"
+ assert by_id[10].helper == "10.0.0.5"
+ assert by_id[20].name == "VOICE"
+
+
+def test_parse_config_access_switch_is_not_core():
+ from services.config_parser import parse_config
+
+ cfg = parse_config('hostname "access-sw-09"\nvlan 1\n untagged 1-48\n')
+ assert cfg.role == "access"
+ assert cfg.hostname == "access-sw-09"
+
+
+def test_parse_config_empty_input_is_safe():
+ from services.config_parser import parse_config
+
+ cfg = parse_config("")
+ assert cfg.hostname == ""
+ assert cfg.vlans == []
+ assert cfg.radius_servers == []