Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions AUDIT.md
Original file line number Diff line number Diff line change
@@ -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.
34 changes: 34 additions & 0 deletions PERSONA.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a href="https://github.com/OneByJorah/NetCore/stargazers"><img src="https://img.shields.io/github/stars/OneByJorah/NetCore?style=flat-square" alt="Stars"></a>
<a href="https://github.com/OneByJorah/NetCore/commits"><img src="https://img.shields.io/github/last-commit/OneByJorah/NetCore?style=flat-square" alt="Last commit"></a>
Expand All @@ -13,7 +13,7 @@
<img src="https://img.shields.io/badge/FastAPI-009688?style=flat-square&logo=fastapi&logoColor=white" alt="FastAPI">
<img src="https://img.shields.io/badge/Electron-desktop-47848F?style=flat-square&logo=electron&logoColor=white" alt="Electron desktop">

![NetCore screenshot](docs/assets/screenshot.png)
![NetCore dashboard](docs/assets/screenshots/01-dashboard.png)

</div>

Expand All @@ -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.
Expand Down
50 changes: 45 additions & 5 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

Loads settings from environment variables (via .env file or system env).
"""
import os

from pydantic_settings import BaseSettings
from typing import Optional


class Settings(BaseSettings):
Expand All @@ -25,16 +24,18 @@ def database_url_safe(self) -> str:
return url

# OpenAI / AI
OPENAI_API_KEY: Optional[str] = None
# 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: str | None = 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"
Expand All @@ -50,3 +51,42 @@ 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 # noqa: E402 (intentionally late; groups the config validation block)

if settings.DEBUG:
warnings.warn(
"DEBUG mode is enabled. Do not use in production.",
RuntimeWarning,
stacklevel=2,
)
6 changes: 2 additions & 4 deletions backend/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

Supports both SQLite (dev) and PostgreSQL (production) seamlessly.
"""
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base

from config import settings
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker

DATABASE_URL = settings.database_url_safe

Expand Down
20 changes: 15 additions & 5 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,23 @@
import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv

from config import settings
from database import init_db
from routers import switches, configs, chat, workflows, dashboard, security, containerlab, templates, discovery, config_parser_router
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routers import (
chat,
config_parser_router,
configs,
containerlab,
dashboard,
discovery,
security,
switches,
templates,
workflows,
)
from services.template_engine import seed_builtin_templates

# Load environment variables
Expand Down
7 changes: 3 additions & 4 deletions backend/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
Models: Switch, ConfigBackup, ChatMessage, Workflow, WorkflowStep,
AuditLog, SecurityFinding, ContainerlabTopology, DeviceMetric.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Float, Boolean, JSON
from sqlalchemy.sql import func
from database import Base
from sqlalchemy import JSON, Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Text
from sqlalchemy.sql import func


class Switch(Base):
Expand Down Expand Up @@ -68,7 +67,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)
Expand Down
2 changes: 1 addition & 1 deletion backend/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# Nethermind — API Routers
# NetCore — API Routers
9 changes: 4 additions & 5 deletions backend/routers/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@

Streaming chat with tool calling via SSE (Server-Sent Events).
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from database import get_db
from fastapi import APIRouter, Depends, HTTPException
from models import ChatMessage
from schemas import ChatRequest, ChatMessageOut
from schemas import ChatMessageOut, ChatRequest
from services.nethermind_agent import ask
from sqlalchemy.orm import Session

router = APIRouter(prefix="/api/chat", tags=["chat"])


@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)
Expand Down
Loading
Loading