diff --git a/backend/api/profile.py b/backend/api/profile.py index c1085bc..c8b6c8d 100644 --- a/backend/api/profile.py +++ b/backend/api/profile.py @@ -1,3 +1,4 @@ +import uuid from fastapi import APIRouter, Depends, status from backend import models, schemas from backend.db import get_db @@ -30,6 +31,7 @@ def create_profile(profile: schemas.ProfileBaseSchema, db: Session = Depends(get Create a new profile. """ db_profile = models.Profile(**profile.model_dump()) + db_profile.profile_id = str(uuid.uuid4()) db.add(db_profile) db.commit() db.refresh(db_profile) diff --git a/backend/api/scan.py b/backend/api/scan.py index 3a77c73..f2c6eed 100644 --- a/backend/api/scan.py +++ b/backend/api/scan.py @@ -1,9 +1,23 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends +from backend.db import get_db from scan.nmap import NmapScanner +from sqlalchemy.orm import Session router = APIRouter() +@router.get("/scan/profile/ping/{profile_id}", tags=["scan"]) +async def scan_profile(profile_id: str, db: Session = Depends(get_db)): + """ + Scan the target profile for ping and traceroute. + + In NMap terms, it runs a basic no-port-scan (nmap -sn --traceroute). + """ + nmap_scanner = NmapScanner() + result = nmap_scanner.scan_profile(profile_id, db) + return {"result": result} + + @router.get("/scan/detailed/{ip_address:path}", tags=["scan"]) async def scan_detailed(ip_address: str): """ diff --git a/backend/db.py b/backend/db.py index a1d4cc6..657ed7b 100644 --- a/backend/db.py +++ b/backend/db.py @@ -1,12 +1,11 @@ from sqlalchemy import create_engine -from sqlalchemy.orm import declarative_base +from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker SQLITE_DATABASE_URL = "sqlite:///./probus.db" engine = create_engine(SQLITE_DATABASE_URL, echo=True, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - Base = declarative_base() diff --git a/backend/main.py b/backend/main.py index 1ae2c69..1521415 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,7 +4,7 @@ from api import scan, default, info, profile from loguru import logger from db import engine -from backend import models +from . import models models.Base.metadata.create_all(bind=engine) diff --git a/backend/models.py b/backend/models.py index 157ab7c..2ad33ed 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1,8 +1,7 @@ -from pydantic import ConfigDict +import uuid +from sqlalchemy import UUID, Column, DateTime, String from db import Base -from sqlalchemy import TIMESTAMP, Column, String from sqlalchemy.sql import func -import uuid class Profile(Base): @@ -11,10 +10,43 @@ class Profile(Base): """ __tablename__ = "profile" - model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) - - profile_id = Column(String, primary_key=True, default=str(uuid.uuid4())) + profile_id = Column(String, primary_key=True, index=True) profile_name = Column(String, nullable=False) + profile_description = Column(String, nullable=True) ip_range = Column(String, nullable=False) - created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now()) - updated_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()) + last_scan = Column(DateTime, nullable=True) + created_at = Column(DateTime, default=func.now(), nullable=False) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) + + +class Scan(Base): + """ + Scan model + """ + + __tablename__ = "scan_history" + scan_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + profile_id = Column(String, nullable=False) + scan_command = Column(String, nullable=True) # "@args": "/opt/homebrew/bin/nmap -sn -T4 -oX - 192.168.1.0/24", + scan_start = Column(DateTime, nullable=True) + scan_end = Column(DateTime, nullable=True) + scan_results_json = Column(String, nullable=True) + + +class Host(Base): + """ + Host model + """ + + __tablename__ = "host" + host_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + scan_id = Column(UUID(as_uuid=True), nullable=False) + ip_address = Column(String, nullable=False) + hostname = Column(String, nullable=True) + os = Column(String, nullable=True) + os_accuracy = Column(String, nullable=True) + scan_summary = Column(String, nullable=True) + scan_start = Column(DateTime, nullable=True) + scan_end = Column(DateTime, nullable=True) + created_at = Column(DateTime, default=func.now(), nullable=False) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) diff --git a/backend/requirements.txt b/backend/requirements.txt index 6ebf8fe..1e4cfc5 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,8 +5,8 @@ fastapi[standard]~=0.115.4 uvicorn~=0.32.0 FindMyIP~=1.2.0 SQLAlchemy~=2.0.36 -sqlmodel~=0.0.22 pydantic~=2.10.3 +sqlalchemy fastapi-utils # Testing diff --git a/backend/scan/nmap.py b/backend/scan/nmap.py index cdbe61a..89d0c88 100644 --- a/backend/scan/nmap.py +++ b/backend/scan/nmap.py @@ -4,6 +4,8 @@ from loguru import logger import xmltodict import json +from sqlalchemy.orm import Session +from backend import models from .nmap_utils import is_root @@ -151,6 +153,28 @@ def list_scan(self, target: str) -> str: self.scan_type = "list" return self.__scan() + @is_root + def scan_profile(self, profile_id: str, db: Session) -> str: + """ + Given a profile ID, scan the target IP address(es) and return the results. + """ + profile = db.query(models.Profile).filter(models.Profile.profile_id == profile_id).first() + if not profile: + logger.error("Profile not found.") + return None + + # get the target IP address(es) from the profile + self.target = profile.ip_range + self.scan_type = "detailed" + if not self.target or not self.scan_type: + logger.error("Target and scan type must be provided.") + return "Target and scan type must be provided." + + # scan the target IP address(es) + scan_result_xml = self.__scan() + + return scan_result_xml + @is_root def scan(self, target: str, type: str) -> str: # Rethinking this... diff --git a/frontend/package-lock.json b/frontend/package-lock.json index af0a1dd..d55856c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,8 +10,12 @@ "license": "ISC", "dependencies": { "@popperjs/core": "^2.11.8", + "@tanstack/react-table": "^8.20.5", "@vitejs/plugin-react": "^4.3.3", "bootstrap": "^5.3.3", + "fp-ts": "^2.16.9", + "io-ts": "^2.2.22", + "random-word-slugs": "^0.1.7", "react": "^18.3.1", "react-bootstrap": "^2.10.5", "react-dom": "^18.3.1", @@ -1606,6 +1610,39 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/react-table": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz", + "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.20.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2661,6 +2698,12 @@ "license": "ISC", "peer": true }, + "node_modules/fp-ts": { + "version": "2.16.9", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.9.tgz", + "integrity": "sha512-+I2+FnVB+tVaxcYyQkHUq7ZdKScaBlX53A41mxQtpIccsfyv8PzdzP7fzp2AY832T4aoK6UZ5WRX/ebGd8uZuQ==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2785,6 +2828,15 @@ "loose-envify": "^1.0.0" } }, + "node_modules/io-ts": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.22.tgz", + "integrity": "sha512-FHCCztTkHoV9mdBsHpocLpdTAfh956ZQcIkWQxxS0U5HT53vtrcuYdQneEJKH6xILaLNzXVl2Cvwtoy8XNN0AA==", + "license": "MIT", + "peerDependencies": { + "fp-ts": "^2.5.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3262,6 +3314,12 @@ ], "license": "MIT" }, + "node_modules/random-word-slugs": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/random-word-slugs/-/random-word-slugs-0.1.7.tgz", + "integrity": "sha512-8cyzxOIDeLFvwSPTgCItMXHGT5ZPkjhuFKUTww06Xg1dNMXuGxIKlARvS7upk6JXIm41ZKXmtlKR1iCRWklKmg==", + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index c26f9b7..147bf07 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,8 +22,12 @@ }, "dependencies": { "@popperjs/core": "^2.11.8", + "@tanstack/react-table": "^8.20.5", "@vitejs/plugin-react": "^4.3.3", "bootstrap": "^5.3.3", + "fp-ts": "^2.16.9", + "io-ts": "^2.2.22", + "random-word-slugs": "^0.1.7", "react": "^18.3.1", "react-bootstrap": "^2.10.5", "react-dom": "^18.3.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6a5a75b..39a75e2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,8 @@ import { Route, Routes } from "react-router-dom"; import Header from "./components/header"; import Home from "./routes/home"; +import Profiles from "./routes/profiles"; +import ScanResults from "./routes/scan_results"; // const App = () => { return ( @@ -9,6 +11,8 @@ const App = () => {
} /> + } /> + } />
diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx index 5dfafaa..5c2e084 100644 --- a/frontend/src/components/header.tsx +++ b/frontend/src/components/header.tsx @@ -15,14 +15,16 @@ const Header = () => { data-bs-theme="dark" > - Probus + + {"{"} Probus {"}"} +