Skip to content

Commit

Permalink
added profile management
Browse files Browse the repository at this point in the history
  • Loading branch information
royrusso committed Dec 15, 2024
1 parent 660bb9a commit 74afa6f
Show file tree
Hide file tree
Showing 19 changed files with 706 additions and 98 deletions.
2 changes: 2 additions & 0 deletions backend/api/profile.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from fastapi import APIRouter, Depends, status
from backend import models, schemas
from backend.db import get_db
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 15 additions & 1 deletion backend/api/scan.py
Original file line number Diff line number Diff line change
@@ -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):
"""
Expand Down
3 changes: 1 addition & 2 deletions backend/db.py
Original file line number Diff line number Diff line change
@@ -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()


Expand Down
2 changes: 1 addition & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
48 changes: 40 additions & 8 deletions backend/models.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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)
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions backend/scan/nmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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...
Expand Down
58 changes: 58 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -9,6 +11,8 @@ const App = () => {
<main>
<Routes>
<Route index element={<Home />} />
<Route path="/results/:profile_id" element={<ScanResults />} />
<Route path="/profiles" element={<Profiles />} />
</Routes>
</main>
</>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ const Header = () => {
data-bs-theme="dark"
>
<Container>
<Navbar.Brand href="#home">Probus</Navbar.Brand>
<Navbar.Brand href="/">
{"{"} Probus {"}"}
</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse
id="basic-navbar-nav"
className="justify-content-end"
>
<Nav className="justify-content-end">
<Nav.Link href="#home">Profiles</Nav.Link>
<Nav.Link href="/profiles">Profiles</Nav.Link>
<Nav.Link href="#home">Settings</Nav.Link>
<NavDropdown title="Dropdown" id="basic-nav-dropdown">
<NavDropdown.Item href="#action/3.1">Action</NavDropdown.Item>
Expand Down
10 changes: 3 additions & 7 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
* Custom class for the react-icons that are wrapped by a provider
*/
.react-icon-button {
display: inline-block;
font-size: 1em;

/* Center the icon */
display: flex;
justify-content: center;
align-items: center;
margin-right: 0.25em;
height: 1.25em;
width: 1.25em;
}
Loading

0 comments on commit 74afa6f

Please sign in to comment.