88 |
89 | ## Gallery Images
90 |
91 | You can see the images of the frontend here: [gallery.md](gallery.md).
92 |
93 |
94 | ## 👨💻 Contributions:
95 |
96 | We welcome contributions to this project! Please feel free to fork the repository and create pull requests.
97 |
98 |
99 | ## 💼 License:
100 |
101 | This project is licensed under the MIT License.
102 |
--------------------------------------------------------------------------------
/backend/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .gitignore
3 | .gitattributes
4 |
5 | docker-compose.yml
6 | Dockerfile
7 | .docker
8 | .dockerignore
9 |
10 | .env
11 | .venv/
12 | venv/
13 |
14 | .idea
15 | .vscode/
16 |
17 | **/__pycache__/
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | *.pyc
3 | .pytest_cache
4 |
5 | .venv/
6 | venv/
7 |
8 | .ruff_cache
9 | .idea
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.12.3-alpine3.20
2 |
3 | ENV PYTHONUNBUFFERED=1
4 |
5 | WORKDIR /backend
6 |
7 | # Install uv
8 | COPY --from=ghcr.io/astral-sh/uv:0.4.15 /uv /bin/uv
9 |
10 | # Optimizing UV
11 | ENV PATH="/backend/.venv/bin:$PATH"
12 | ENV UV_COMPILE_BYTECODE=1
13 | ENV UV_LINK_MODE=copy
14 |
15 | # Install dependencies
16 | RUN --mount=type=cache,target=/root/.cache/uv \
17 | --mount=type=bind,source=uv.lock,target=uv.lock \
18 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
19 | uv sync --frozen --no-install-project --no-editable
20 |
21 | # Copy the necessary files
22 | COPY pyproject.toml uv.lock ./
23 | COPY src/ ./src/
24 | COPY tests/ ./tests/
25 |
26 | # Sync the project
27 | # Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
28 | RUN --mount=type=cache,target=/root/.cache/uv \
29 | uv sync
30 |
31 | RUN adduser -D appuser && chown -R appuser /backend
32 | USER appuser
33 |
34 | ENV PYTHONPATH=/backend
35 |
36 | CMD ["uv", "run", "/backend/src/main.py"]
37 |
38 |
--------------------------------------------------------------------------------
/backend/README.md:
--------------------------------------------------------------------------------
1 | # Sentinel - Backend
2 |
3 | ## Requirements
4 |
5 | - 🐋 [Docker](https://github.com/docker/compose): for development and production.
6 | - 📦 [UV](https://github.com/astral-sh/uv): An extremely fast Python package and project manager.
7 |
8 | ## Local Development
9 |
10 | You have several options to run the FastAPI backend:
11 |
12 | **Option 1: Python Run**
13 |
14 | Navigate to the `backend` directory and use the following command (make sure your working dir is the `backend`):
15 |
16 | ```commandline
17 | python src/main.py
18 | ```
19 |
20 | **Option 2: Docker Compose**
21 |
22 | Start the stack with Docker Compose:
23 |
24 | ```commandline
25 | docker compose up -d
26 | ```
27 |
28 | - Automatic interactive documentation with Swagger UI (from the OpenAPI backend): http://localhost:5000/docs
29 |
30 | If your Docker is not running in localhost (the URLs above wouldn't work) you would need to use the IP or domain where your Docker is running.
31 |
32 | ## General workflow
33 |
34 | By default, the dependencies are managed with [UV](https://github.com/astral-sh/uv),
35 | Installation can be found from standalone installers or from [PyPI](https://pypi.org/project/uv/):
36 | ```commandline
37 | # On macOS and Linux.
38 | $ curl -LsSf https://astral.sh/uv/install.sh | sh
39 |
40 | # On Windows.
41 | $ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
42 |
43 | # With pip.
44 | $ pip install uv
45 | ```
46 |
47 | From `./backend/` you can install all the dependencies with:
48 |
49 | ```commandline
50 | $ uv sync
51 | ```
52 |
53 | Then you can activate the virtual environment with:
54 |
55 | ```commandline
56 | $ source .venv/bin/activate
57 | ```
58 |
59 | Make sure your editor is using the correct Python virtual environment, with the interpreter at `backend/.venv/bin/python`.
60 |
61 | ## Modify the Configuration with Environment Variables:
62 |
63 | To configure the application, utilize environment files (`.env`) to specify settings such as database connection details, application port, and other environment-specific configurations.
64 |
65 | **Environment Settings:**
66 | The application supports configuration based on environment settings for both development and production environments. You can find the configuration settings in `app.py`, `db.py`, and `metadata.py`, all residing within the directory `backend/src/core/config/`.
67 |
68 | Each configuration is designated with a distinct environment variable prefix: `BACKEND_`, `DB_`, `METADATA_`.
69 |
70 | **Production Environment Setup:**
71 | The system checks for the `BACKEND_ENVIRONMENT` environment variable to determine if the environment is a production environment. This allows for overriding of reload settings and the setup of workers as necessary.
72 |
73 | **Docker Integration**
74 | If you plan on using Docker, the `.dockerignore` file is configured to ignore the environment files. This setup allows you to load the environment variables specifically for Docker Compose usage, providing flexibility for both development and production environments. You can also set up separate Docker Compose files for development and production (`docker-compose.dev.yml` and `docker-compose.prod.yml`) to ensure consistency across environments.
75 |
76 | Before deploying it, make sure you change at least the values for the database.
77 |
--------------------------------------------------------------------------------
/backend/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "backend"
3 | version = "0.1.0"
4 | description = ""
5 | requires-python = ">=3.10,<4.0"
6 | dependencies = [
7 | "uvicorn<1.0.0,>=0.30.1",
8 | "fastapi[standard]<1.0.0,>=0.111.0",
9 | "python-multipart<1.0.0,>=0.0.7",
10 | "pydantic-settings<3.0.0,>=2.2.0",
11 | "pydantic<3.0.0,>=2.0",
12 | "pyjwt<3.0.0,>=2.8.0",
13 | "passlib[bcrypt]<2.0.0,>=1.7.4",
14 | "bcrypt==4.0.1",
15 | # Pin bcrypt until passlib supports the latest
16 | "beanie<2.0.0,>=1.26.0",
17 | ]
18 |
19 | [tool.uv]
20 | dev-dependencies = [
21 | "pytest-asyncio>=0.24.0",
22 | "pytest<9.0.0,>=7.4.3",
23 | "ruff<1.0.0,>=0.6.9",
24 | ]
25 |
26 | [tool.ruff]
27 | target-version = "py310"
28 |
29 | [tool.ruff.lint]
30 | select = [
31 | "E", # pycodestyle errors
32 | "W", # pycodestyle warnings
33 | "F", # pyflakes
34 | "I", # isort
35 | "B", # flake8-bugbear
36 | "C4", # flake8-comprehensions
37 | "UP", # pyupgrade
38 | "ARG001", # unused arguments in functions
39 | ]
40 | ignore = [
41 | "E501", # line too long, handled by black
42 | "B008", # do not perform function calls in argument defaults
43 | "W191", # indentation contains tabs
44 | "B904", # Allow raising exceptions without from e, for HTTPException
45 | ]
46 |
47 | [tool.ruff.lint.pyupgrade]
48 | # Preserve types, even if a file imports `from __future__ import annotations`.
49 | keep-runtime-typing = true
50 |
--------------------------------------------------------------------------------
/backend/src/api/app.py:
--------------------------------------------------------------------------------
1 | from contextlib import asynccontextmanager
2 |
3 | from fastapi import FastAPI
4 | from fastapi.middleware.cors import CORSMiddleware
5 |
6 | from src.api.routers.auth import router as auth_router
7 | from src.api.routers.device import router as devices_router
8 | from src.api.routers.statistics import router as statistics_router
9 | from src.api.routers.user import router as users_router
10 | from src.core.config.provider import ConfigProvider
11 | from src.core.db import init_db
12 |
13 |
14 | @asynccontextmanager
15 | async def lifespan(instance: FastAPI):
16 | _ = instance
17 | await init_db()
18 | yield
19 |
20 | config = ConfigProvider()
21 | app_metadata = config.metadata()
22 | app_config = config.app_settings()
23 |
24 | app = FastAPI(root_path="/api",
25 | title=app_metadata.PROJECT_NAME,
26 | description=app_metadata.PROJECT_DESCRIPTION,
27 | version=app_metadata.VERSION,
28 | lifespan=lifespan,
29 | responses={404: {"description": "Not found"}})
30 |
31 | if app_config.CORS_ORIGINS:
32 | app.add_middleware(
33 | CORSMiddleware,
34 | allow_origins=[
35 | str(origin).strip("/") for origin in app_config.CORS_ORIGINS
36 | ],
37 | allow_credentials=True,
38 | allow_methods=["GET", "POST", "PUT", "DELETE"],
39 | allow_headers=["*"],
40 | )
41 |
42 | app.include_router(users_router)
43 | app.include_router(devices_router)
44 | app.include_router(statistics_router)
45 | app.include_router(auth_router)
46 |
--------------------------------------------------------------------------------
/backend/src/api/controllers/auth.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 |
3 | from fastapi import Depends, HTTPException, status
4 | from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
5 |
6 | import src.api.models.user as user_model
7 | from src.core.security.password import verify_password
8 | from src.core.security.token import (
9 | ACCESS_TOKEN_EXPIRE_MINUTES,
10 | Token,
11 | create_access_token,
12 | decode_jwt_token,
13 | )
14 | from src.dal.entities.user import User
15 |
16 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token/")
17 |
18 |
19 | async def authenticate_user(username: str, password: str) -> User:
20 | user = await user_model.get_user_by_username(username)
21 | if not user:
22 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
23 | detail="Authentication failed invalid credentials")
24 |
25 | hashed_password = user.hashed_password
26 | if not verify_password(password, hashed_password):
27 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
28 | detail="Authentication failed invalid credentials")
29 | return user
30 |
31 |
32 | async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
33 | decoded_token_username = await decode_jwt_token(token)
34 | user = await user_model.get_user_by_username(decoded_token_username)
35 | if user is None:
36 | raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
37 | detail="Authentication failed invalid credentials")
38 | return user
39 |
40 |
41 | async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()) -> Token:
42 | user = await authenticate_user(form_data.username, form_data.password)
43 | if not user:
44 | raise HTTPException(
45 | status_code=status.HTTP_401_UNAUTHORIZED,
46 | detail="Incorrect username or password",
47 | headers={"WWW-Authenticate": "Bearer"},
48 | )
49 | access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
50 | access_token = create_access_token(
51 | data={"sub": user.username}, expires_delta=access_token_expires
52 | )
53 | return Token(access_token=access_token, token_type="bearer")
54 |
--------------------------------------------------------------------------------
/backend/src/api/controllers/device.py:
--------------------------------------------------------------------------------
1 | from beanie import PydanticObjectId
2 |
3 | import src.api.models.device as device_model
4 | from src.dal.entities.device import Device
5 |
6 |
7 | async def get_all_devices() -> list[Device]:
8 | devices = await device_model.get_all_devices()
9 | return devices
10 |
11 |
12 | async def get_device(device_id: PydanticObjectId) -> Device:
13 | device = await device_model.get_device_by_id(device_id)
14 | return device
15 |
--------------------------------------------------------------------------------
/backend/src/api/controllers/statistics/device.py:
--------------------------------------------------------------------------------
1 | # TODO: statistics collection with a 24h cache
2 | import src.api.models.device as device_model
3 |
4 |
5 | async def get_devices_count() -> int:
6 | devices_count = await device_model.get_all_devices_count()
7 | return devices_count
8 |
9 |
10 | def get_os_distribution() -> dict:
11 | # TODO: collect real data
12 | os_distribution = {
13 | "windows": 100,
14 | "macos": 50,
15 | "linux": 30
16 | }
17 | return os_distribution
18 |
19 |
20 | def get_security_software_coverage() -> dict:
21 | # TODO: collect real data
22 | security_software_coverage = {
23 | "sentinel": 1200,
24 | "mcafee": 7100,
25 | }
26 | return security_software_coverage
27 |
--------------------------------------------------------------------------------
/backend/src/api/controllers/user.py:
--------------------------------------------------------------------------------
1 | from beanie import PydanticObjectId
2 |
3 | import src.api.models.user as user_model
4 | from src.dal.entities.user import User
5 |
6 |
7 | async def create_user(username: str, password: str) -> User:
8 | user = await user_model.create_user(username, password)
9 | return user
10 |
11 |
12 | async def get_all_users() -> list[User]:
13 | users = await user_model.get_all_users()
14 | return users
15 |
16 |
17 | async def get_user_by_id(user_id: PydanticObjectId) -> User:
18 | user = await user_model.get_user_by_id(user_id)
19 | return user
20 |
21 |
22 | async def get_user_by_username(username: str) -> User:
23 | user = await user_model.get_user_by_username(username)
24 | return user
25 |
--------------------------------------------------------------------------------
/backend/src/api/models/device.py:
--------------------------------------------------------------------------------
1 | from beanie import PydanticObjectId
2 |
3 | import src.dal.repositories.device as device_repository
4 | from src.dal.entities.device import Device
5 |
6 |
7 | async def create_device(device_data: dict) -> Device:
8 | return await device_repository.create_device(device_data)
9 |
10 |
11 | async def get_device_by_id(device_id: PydanticObjectId) -> Device:
12 | return await device_repository.get_device_by_id(device_id)
13 |
14 |
15 | async def get_all_devices() -> list[Device]:
16 | return await device_repository.get_all_devices()
17 |
18 |
19 | async def get_all_devices_count() -> int:
20 | return await device_repository.get_all_devices_count()
21 |
22 |
23 | async def get_devices_by_os_type(os_type: str) -> list[Device]:
24 | return await device_repository.get_devices_by_os_type(os_type)
25 |
26 |
27 | async def update_device(device_id: PydanticObjectId, updated_data: dict) -> bool:
28 | return await device_repository.update_device(device_id, updated_data)
29 |
30 |
31 | async def remove_device_by_id(device_id: PydanticObjectId) -> bool:
32 | return await device_repository.remove_device_by_id(device_id)
33 |
--------------------------------------------------------------------------------
/backend/src/api/models/user.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from beanie import PydanticObjectId
4 | from fastapi import HTTPException, status
5 |
6 | import src.dal.repositories.user as user_repository
7 | from src.core.security.password import hash_password
8 | from src.dal.entities.user import User
9 |
10 |
11 | async def create_user(username: str, password: str) -> User:
12 | user = await user_repository.get_user_by_username(username)
13 | if user:
14 | raise HTTPException(status_code=status.HTTP_409_CONFLICT,
15 | detail=f"User already exists with '{username}' username")
16 |
17 | hashed_password = hash_password(password)
18 | user_data = {
19 | 'username': username,
20 | 'hashed_password': hashed_password,
21 | 'last_seen': datetime.utcnow(),
22 | 'registration_date': datetime.utcnow()
23 | }
24 | return await user_repository.create_user(user_data)
25 |
26 |
27 | async def get_all_users() -> list[User]:
28 | return await user_repository.get_all_users()
29 |
30 |
31 | async def get_user_by_username(username: str) -> User:
32 | user = await user_repository.get_user_by_username(username)
33 | if not user:
34 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
35 | detail=f"User with username '{username}' not found")
36 | return user
37 |
38 |
39 | async def get_user_by_id(user_id: PydanticObjectId) -> User:
40 | user = await user_repository.get_user_by_uuid(user_id)
41 | if not user:
42 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"User with ID '{user_id}' not found")
43 | return user
44 |
45 |
46 | async def update_user_password(username: str, new_password: str) -> None:
47 | user = await get_user_by_username(username)
48 | await user_repository.update_user_password(user, new_password)
49 |
50 |
51 | async def delete_user_by_username(username: str) -> None:
52 | user = await get_user_by_username(username)
53 | await user_repository.delete_user(user)
54 |
55 |
56 | async def delete_user_by_id(user_id: PydanticObjectId) -> None:
57 | user = await get_user_by_id(user_id)
58 | await user_repository.delete_user(user)
59 |
--------------------------------------------------------------------------------
/backend/src/api/routers/auth.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Depends
2 |
3 | import src.api.controllers.auth as auth_controller
4 | from src.core.security.token import Token
5 |
6 | router = APIRouter(prefix="/auth", tags=["auth"])
7 |
8 |
9 | @router.post("/token/")
10 | async def login_token(token: Token = Depends(auth_controller.login_for_access_token)) -> Token:
11 | return token
12 |
--------------------------------------------------------------------------------
/backend/src/api/routers/device.py:
--------------------------------------------------------------------------------
1 | from beanie import PydanticObjectId
2 | from fastapi import APIRouter
3 |
4 | import src.api.controllers.device as device_controller
5 | from src.dal.entities.device import Device
6 |
7 | router = APIRouter(prefix="/device", tags=["device"])
8 |
9 |
10 | @router.get("/")
11 | async def read_devices() -> list[Device]:
12 | devices = await device_controller.get_all_devices()
13 | return devices
14 |
15 |
16 | @router.get("/{device_id}")
17 | async def read_device(device_id: PydanticObjectId) -> Device:
18 | device = await device_controller.get_device(device_id)
19 | return device
20 |
--------------------------------------------------------------------------------
/backend/src/api/routers/statistics/__init__.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 |
3 | from .device import router as devices_statistics_router
4 |
5 | router = APIRouter(prefix="/statistics", tags=["statistics"])
6 |
7 | router.include_router(devices_statistics_router)
8 |
--------------------------------------------------------------------------------
/backend/src/api/routers/statistics/device.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 |
3 | import src.api.controllers.statistics.device as devices_stats_controller
4 |
5 | router = APIRouter(prefix="/device", tags=["device"])
6 |
7 |
8 | @router.get("/total-count")
9 | async def read_total_count() -> int:
10 | total_count = await devices_stats_controller.get_devices_count()
11 | return total_count
12 |
13 |
14 | @router.get("/os-distribution")
15 | async def read_os_distribution() -> dict:
16 | os_distribution = devices_stats_controller.get_os_distribution()
17 | return os_distribution
18 |
19 |
20 | @router.get("/security-software-coverage")
21 | async def read_security_software_coverage() -> dict:
22 | security_software_coverage = devices_stats_controller.get_security_software_coverage()
23 | return security_software_coverage
24 |
--------------------------------------------------------------------------------
/backend/src/api/routers/user.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Depends
2 |
3 | import src.api.controllers.auth as auth_controller
4 | import src.api.controllers.user as user_controller
5 | from src.dal.entities.user import User
6 |
7 | router = APIRouter(prefix="/user", tags=["user"])
8 |
9 |
10 | @router.post("/add")
11 | async def add_user(username: str, password: str) -> User:
12 | user = await user_controller.create_user(username, password)
13 | return user
14 |
15 |
16 | @router.get("/")
17 | async def read_users() -> list[User]:
18 | users = await user_controller.get_all_users()
19 | return users
20 |
21 |
22 | @router.get("/me")
23 | async def read_user_me(current_user: User = Depends(auth_controller.get_current_user)):
24 | return current_user
25 |
26 |
27 | @router.get("/{username}")
28 | async def read_user(username: str) -> User:
29 | user = await user_controller.get_user_by_username(username)
30 | return user
31 |
--------------------------------------------------------------------------------
/backend/src/core/config/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import secrets
3 | from typing import Annotated, Any
4 |
5 | from pydantic import AnyUrl, BeforeValidator
6 | from pydantic_settings import BaseSettings, SettingsConfigDict
7 |
8 | from src.core.config.environment import Environment
9 |
10 |
11 | def parse_cors(v: Any) -> list[str] | str:
12 | if isinstance(v, str) and not v.startswith("["):
13 | return [i.strip() for i in v.split(",")]
14 | elif isinstance(v, list | str):
15 | return v
16 | raise ValueError(v)
17 |
18 |
19 | def parse_environment(v: str) -> Environment:
20 | if isinstance(v, Environment):
21 | return v
22 | if isinstance(v, str):
23 | return Environment(v.lower())
24 | raise ValueError(v)
25 |
26 |
27 | class AppSettings(BaseSettings):
28 | model_config = SettingsConfigDict(env_file='.env',
29 | env_prefix='BACKEND_',
30 | env_ignore_empty=True,
31 | extra="ignore")
32 |
33 | DOMAIN: str = "0.0.0.0"
34 | PORT: int = 5000
35 | CORS_ORIGINS: Annotated[list[AnyUrl] | str, BeforeValidator(parse_cors)] = []
36 | ENVIRONMENT: Annotated[Environment, BeforeValidator(parse_environment)] = Environment.DEVELOPMENT
37 | WORKER_COUNT: int = os.cpu_count() * 2 + 1
38 |
39 | SECRET_KEY: str = secrets.token_urlsafe(32)
40 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7
41 |
--------------------------------------------------------------------------------
/backend/src/core/config/db.py:
--------------------------------------------------------------------------------
1 | from pydantic import computed_field
2 | from pydantic_settings import BaseSettings, SettingsConfigDict
3 |
4 |
5 | class DBSettings(BaseSettings):
6 | model_config = SettingsConfigDict(env_file='.env',
7 | env_prefix='DB_',
8 | env_ignore_empty=True,
9 | extra="ignore")
10 |
11 | SERVER: str = "localhost"
12 | PORT: int = 27017
13 | USER: str
14 | PASSWORD: str
15 | NAME: str = "sentinel"
16 |
17 | @computed_field
18 | @property
19 | def MONGO_DATABASE_URI(self) -> str:
20 | uri = f"mongodb://{self.USER}:{self.PASSWORD}@{self.SERVER}:{self.PORT}/{self.NAME}"
21 | return uri
22 |
--------------------------------------------------------------------------------
/backend/src/core/config/environment.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class Environment(Enum):
5 | PRODUCTION = "production"
6 | DEVELOPMENT = "development"
7 |
--------------------------------------------------------------------------------
/backend/src/core/config/metadata.py:
--------------------------------------------------------------------------------
1 | from pydantic_settings import BaseSettings, SettingsConfigDict
2 |
3 |
4 | class Metadata(BaseSettings):
5 | model_config = SettingsConfigDict(env_file='.env',
6 | env_prefix='METADATA_',
7 | env_ignore_empty=True,
8 | extra="ignore")
9 |
10 | PROJECT_NAME: str = "Sentinel"
11 | PROJECT_DESCRIPTION: str = "A user-friendly Command & Control platform API."
12 | MAINTAINER: str = "Shahar Band"
13 | VERSION: str = "1.0.0"
14 |
--------------------------------------------------------------------------------
/backend/src/core/config/provider.py:
--------------------------------------------------------------------------------
1 | from functools import lru_cache
2 |
3 | from src.core.config.app import AppSettings
4 | from src.core.config.db import DBSettings
5 | from src.core.config.metadata import Metadata
6 |
7 |
8 | class ConfigProvider:
9 | @staticmethod
10 | @lru_cache(maxsize=1)
11 | def app_settings() -> AppSettings:
12 | return AppSettings()
13 |
14 | @staticmethod
15 | @lru_cache(maxsize=1)
16 | def db_settings() -> DBSettings:
17 | return DBSettings()
18 |
19 | @staticmethod
20 | @lru_cache(maxsize=1)
21 | def metadata() -> Metadata:
22 | return Metadata()
23 |
--------------------------------------------------------------------------------
/backend/src/core/db.py:
--------------------------------------------------------------------------------
1 | import motor.motor_asyncio
2 | from beanie import init_beanie
3 |
4 | from src.core.config.provider import ConfigProvider
5 | from src.dal.entities.device import Device
6 | from src.dal.entities.user import User
7 |
8 |
9 | async def init_db():
10 | db_settings = ConfigProvider.db_settings()
11 |
12 | client = motor.motor_asyncio.AsyncIOMotorClient(
13 | db_settings.MONGO_DATABASE_URI
14 | )
15 | db = client[db_settings.NAME]
16 |
17 | await init_beanie(database=db,
18 | document_models=[
19 | Device,
20 | User
21 | ])
22 |
--------------------------------------------------------------------------------
/backend/src/core/security/password.py:
--------------------------------------------------------------------------------
1 | from passlib.context import CryptContext
2 |
3 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
4 |
5 |
6 | def hash_password(password: str) -> str:
7 | return pwd_context.hash(password)
8 |
9 |
10 | def verify_password(plain_password: str, hashed_password: str) -> bool:
11 | return pwd_context.verify(plain_password, hashed_password)
12 |
--------------------------------------------------------------------------------
/backend/src/core/security/token.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta, timezone
2 |
3 | import jwt
4 | from jwt import DecodeError, InvalidTokenError
5 | from pydantic import BaseModel
6 |
7 | from src.core.config.provider import ConfigProvider
8 |
9 | app_settings = ConfigProvider.app_settings()
10 | SECRET_KEY = app_settings.SECRET_KEY
11 | ACCESS_TOKEN_EXPIRE_MINUTES = app_settings.ACCESS_TOKEN_EXPIRE_MINUTES
12 | ALGORITHM = "HS256"
13 |
14 |
15 | class Token(BaseModel):
16 | access_token: str
17 | token_type: str
18 |
19 |
20 | def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
21 | to_encode = data.copy()
22 | if expires_delta:
23 | expire = datetime.now(timezone.utc) + expires_delta
24 | else:
25 | expire = datetime.now(timezone.utc) + timedelta(minutes=15)
26 | to_encode.update({"exp": expire})
27 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
28 | return encoded_jwt
29 |
30 |
31 | async def decode_jwt_token(token: str) -> str:
32 | try:
33 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
34 | username: str = payload.get("sub")
35 | if username is None:
36 | raise DecodeError("Username is missing in token payload")
37 |
38 | except InvalidTokenError:
39 | raise InvalidTokenError("Invalid token")
40 |
41 | return username
42 |
--------------------------------------------------------------------------------
/backend/src/dal/entities/device.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from beanie import Document
4 | from pydantic import IPvAnyAddress, StrictStr
5 |
6 |
7 | class Device(Document):
8 | name: StrictStr
9 | os_type: StrictStr
10 | ip_address: IPvAnyAddress
11 | description: StrictStr
12 | last_update: datetime
13 | registration_date: datetime
14 |
15 | class Settings:
16 | name = "device"
17 |
18 | def __repr__(self):
19 | return f'Device(id={self.id}, name={self.name}, os_type={self.os_type}, ip_address={self.ip_address})'
20 |
--------------------------------------------------------------------------------
/backend/src/dal/entities/devices/linux.py:
--------------------------------------------------------------------------------
1 | from pydantic import StrictStr
2 |
3 | from src.dal.entities.device import Device
4 |
5 |
6 | class LinuxDevice(Device):
7 | linux_distribution: StrictStr
8 | linux_kernel_version: StrictStr
9 |
--------------------------------------------------------------------------------
/backend/src/dal/entities/devices/windows.py:
--------------------------------------------------------------------------------
1 | from pydantic import StrictStr
2 |
3 | from src.dal.entities.device import Device
4 |
5 |
6 | class WindowsDevice(Device):
7 | windows_version: StrictStr
8 | windows_update_status: StrictStr
9 |
--------------------------------------------------------------------------------
/backend/src/dal/entities/user.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from beanie import Document
4 | from pydantic import StrictStr
5 |
6 |
7 | class User(Document):
8 | username: StrictStr
9 | hashed_password: StrictStr
10 | last_seen: datetime
11 | registration_date: datetime
12 |
13 | def __repr__(self):
14 | return f""
15 |
--------------------------------------------------------------------------------
/backend/src/dal/repositories/device.py:
--------------------------------------------------------------------------------
1 | from beanie import PydanticObjectId
2 |
3 | from src.dal.entities.device import Device
4 | from src.dal.entities.devices.linux import LinuxDevice
5 | from src.dal.entities.devices.windows import WindowsDevice
6 |
7 | DEVICE_NAME_TO_TYPE = {
8 | "windows": WindowsDevice,
9 | "linux": LinuxDevice,
10 | }
11 |
12 |
13 | def device_factory(device_data: dict) -> Device:
14 | os_type = device_data.get('os_type').lower()
15 | if os_type in DEVICE_NAME_TO_TYPE:
16 | device = DEVICE_NAME_TO_TYPE.get(os_type)(**device_data)
17 | return device
18 | return Device(**device_data)
19 |
20 |
21 | async def create_device(device_data: dict) -> Device:
22 | new_device = device_factory(device_data)
23 | await new_device.insert()
24 | return new_device
25 |
26 |
27 | async def get_device_by_id(device_id: PydanticObjectId) -> Device:
28 | return await Device.get(device_id)
29 |
30 |
31 | async def get_all_devices() -> list[Device]:
32 | return await Device.find_all().to_list()
33 |
34 |
35 | async def get_all_devices_count() -> int:
36 | return await Device.find_all().count()
37 |
38 |
39 | async def get_devices_by_os_type(os_type: str) -> list[Device]:
40 | # TODO: testing this will be required i am not sure
41 | if os_type.lower() in DEVICE_NAME_TO_TYPE:
42 | device_class = DEVICE_NAME_TO_TYPE[os_type.lower()]
43 | return await device_class.find().to_list()
44 | return []
45 |
46 |
47 | async def update_device(device_id: PydanticObjectId, updated_data: dict) -> bool:
48 | device = await Device.get(device_id)
49 | if device:
50 | for key, value in updated_data.items():
51 | device[key] = value
52 | await device.update()
53 | return True
54 | return False
55 |
56 |
57 | async def remove_device_by_id(device_id: PydanticObjectId) -> bool:
58 | device = await Device.get(device_id)
59 | if device:
60 | await device.delete()
61 | return True
62 | return False
63 |
--------------------------------------------------------------------------------
/backend/src/dal/repositories/user.py:
--------------------------------------------------------------------------------
1 | from beanie import PydanticObjectId
2 |
3 | from src.dal.entities.user import User
4 |
5 |
6 | async def create_user(user_data: dict) -> User:
7 | new_user = User(**user_data)
8 | await new_user.insert()
9 | return new_user
10 |
11 |
12 | async def get_all_users() -> list[User]:
13 | return await User.find_all().to_list()
14 |
15 |
16 | async def get_user_by_username(username: str) -> User:
17 | user = await User.find_one(User.username == username)
18 | return user
19 |
20 |
21 | async def get_user_by_uuid(user_uuid: PydanticObjectId) -> User:
22 | user = await User.get(user_uuid)
23 | return user
24 |
25 |
26 | async def update_user_password(user: User, new_password: str) -> None:
27 | user.password = new_password
28 | await user.update()
29 |
30 |
31 | async def delete_user(user: User) -> None:
32 | await user.delete()
33 |
--------------------------------------------------------------------------------
/backend/src/main.py:
--------------------------------------------------------------------------------
1 | import uvicorn
2 |
3 | from src.core.config.environment import Environment
4 | from src.core.config.provider import ConfigProvider
5 |
6 |
7 | def run_server():
8 | app_settings = ConfigProvider.app_settings()
9 | dev_environment = app_settings.ENVIRONMENT == Environment.DEVELOPMENT
10 |
11 | uvicorn.run(
12 | "api.app:app",
13 | host=app_settings.DOMAIN,
14 | port=app_settings.PORT,
15 | reload=dev_environment,
16 | workers=app_settings.WORKER_COUNT if not dev_environment else None
17 | )
18 |
19 |
20 | if __name__ == "__main__":
21 | run_server()
22 |
--------------------------------------------------------------------------------
/backend/tests/integration/test_devices_integration.py:
--------------------------------------------------------------------------------
1 | pass
2 |
--------------------------------------------------------------------------------
/backend/tests/unit/.env:
--------------------------------------------------------------------------------
1 | APP_DOMAIN=localhost
2 | APP_PORT=5000
3 | APP_ENVIRONMENT=development
4 | APP_BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173"
5 |
6 | DB_USER=sentinel
7 | DB_PASSWORD=12345678
8 | DB_SERVER=localhost
9 | DB_PORT=27017
10 | DB_NAME=sentinel
--------------------------------------------------------------------------------
/backend/tests/unit/test_users.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from httpx import AsyncClient
3 |
4 | from src.api.app import app
5 |
6 |
7 | @pytest.mark.anyio
8 | async def test_read_users():
9 | pass
10 | async with AsyncClient(app=app, base_url="http://localhost:5000", follow_redirects=True) as ac:
11 | yield ac
12 | # response = await client_test.get("/api/user/")
13 | # assert response.status_code == 200
14 |
--------------------------------------------------------------------------------
/deployment.md:
--------------------------------------------------------------------------------
1 | # Sentinel - Deployment
2 |
3 | You can deploy the project using Docker Compose to a remote server.
4 |
5 | But you have to configure a couple things first. 🤓
6 |
7 | # Preparation
8 |
9 | * Have a remote server ready and available.
10 | * Setup the required environment variables.
11 | * Create and configure the MongoDB database user.
12 |
13 | # Environment Variables
14 |
15 | * `METADATA_PROJECT_NAME`: The name of the project, used in the API for the docs.
16 | * `METADATA_PROJECT_DESCRIPTION`: The description of the project, used in the API for the docs.
17 | * `METADATA_MAINTAINER`: The project maintainer (currently unused).
18 | * `METADATA_VERSION`: The version of the project, used in the API for the docs.
19 | * `BACKEND_ENVIRONMENT`: The environment setup configuration. It can be either `development` or `production` *it is important to set correctly*.
20 | * `BACKEND_DOMAIN`: The backend host (Default value is `0.0.0.0`).
21 | * `BACKEND_PORT`: The backend api port.
22 | * `BACKEND_CORS_ORIGINS`: A list of allowed CORS origins separated by commas.
23 | * `BACKEND_ACCESS_TOKEN_EXPIRE_MINUTES`: Define the token expiration time in minutes (Default value is `7 days`).
24 | * `BACKEND_SECRET_KEY`: The secret key for the backend, used to sign tokens (Default value is `randomly generated`).
25 | * `BACKEND_WORKER_COUNT`: The backend workers count (Default recommended value is `2x CPU cores + 1`).
26 | * `DB_USER`: The MongoDB user, you must set a value because the URI depends on it.
27 | * `DB_PASSWORD`: The MongoDB user password.
28 | * `DB_SERVER`: The hostname of the MongoDB server. You can leave the default of db, provided by the same Docker Compose. You normally wouldn't need to change this unless you are using a third-party provider.
29 | * `DB_PORT`: The port of the MongoDB server. You can leave the default. You normally wouldn't need to change this unless you are using a third-party provider.
30 | * `DB_NAME`: The database name to use for this application. You can leave the default of `sentinel`.
31 |
32 | You can (and should) pass passwords and secret keys as environment variables from secrets.
33 |
34 |
35 | # Initalizing MongoDB User
36 |
37 | You can initialize the MongoDB user by either:
38 |
39 | - Running the provided setup script on the MongoDB container:
40 |
41 | ```bash
42 | sh scripts/mongo-setup.sh
43 | ```
44 |
45 | If the setup script has incorrect line endings (`CRLF`) and you want to convert them to Unix (`LF`), you can use a tool or command to do so:
46 |
47 | ```bash
48 | sed -i 's/\r$//' ./scripts/mongo-setup.sh
49 | ```
50 | afterwards you can run the shell file again.
51 |
52 | - Manually creating a user using the mongosh CLI.
53 |
54 | # URLs
55 |
56 | Frontend: http://sentinel.example.com:80
57 |
58 | Backend API docs: http://sentinel.example.com:5000/docs / http://sentinel.example.com:5000/redocs
59 |
60 | Backend API base URL:http://sentinel.example.com:5000/api
61 |
62 | * Currently, there is no proxy handling communication to the outside world or managing HTTPS certificates. It is recommended to use Traefik or a similar proxy solution for handling these tasks.
63 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | frontend:
3 | build:
4 | context: ./frontend
5 | args:
6 | NODE_ENV: production
7 | restart: always
8 | depends_on:
9 | - backend
10 | ports:
11 | - 80:80
12 | networks:
13 | - default
14 |
15 | backend:
16 | restart: always
17 | depends_on:
18 | - db
19 | env_file:
20 | - .env
21 | environment:
22 | DB_SERVER: db
23 | build:
24 | context: ./backend
25 | ports:
26 | - ${BACKEND_PORT}:5000
27 | networks:
28 | - default
29 |
30 | db:
31 | image: mongo:5.0-focal
32 | restart: always
33 | volumes:
34 | - mongo-data:/data/db
35 | - ./scripts:/scripts
36 | env_file:
37 | - .env
38 | ports:
39 | - ${DB_PORT}:27017
40 | networks:
41 | - default
42 |
43 | networks:
44 | default:
45 |
46 | volumes:
47 | mongo-data:
48 |
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/frontend/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 0, "build-stage", based on Node.js, to build and compile the frontend
2 | FROM node:22.2.0-alpine as build-stage
3 |
4 | WORKDIR /app
5 |
6 | COPY package*.json /app/
7 |
8 | RUN npm ci
9 |
10 | COPY ./ /app/
11 |
12 | RUN npm run build
13 |
14 | # Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
15 | FROM nginx:latest
16 |
17 | COPY --from=build-stage /app/dist/ /usr/share/nginx/html
18 |
19 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf
20 | COPY ./nginx-backend-not-found.conf /etc/nginx/extra-conf.d/backend-not-found.conf
21 |
22 | CMD ["nginx", "-g", "daemon off;"]
23 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Sentinel - Frontend
2 |
3 | ## Requirements
4 |
5 | - 🐋 [Docker](https://github.com/docker/compose): for development and production.
6 | - 📦 [Node.js](https://nodejs.org/en): JavaScript runtime environment. Includes npm for package management.
7 |
8 | ## Local Development
9 |
10 | You have several options to run the frontend:
11 |
12 | **Option 1: NPM**
13 |
14 | Within the `frontend` directory, install the necessary NPM packages:
15 |
16 | ```
17 | npm install
18 | ```
19 |
20 | And start the live server with the following npm script:
21 |
22 | ```commandline
23 | npm run dev
24 | ```
25 |
26 | - Then open your browser at http://localhost:5173/.
27 |
28 | Notice that this live server is not running inside Docker, it's for local development, and that is the recommended workflow. Once you are happy with your frontend, you can build the frontend Docker image and start it, to test it in a production-like environment. But building the image at every change will not be as productive as running the local development server with live reload.
29 |
30 | Check the file `package.json` to see other available options.
31 |
32 | **Option 2: Docker - NGINX**
33 |
34 | Start the stack with Docker Compose:
35 |
36 | ```commandline
37 | docker compose up -d
38 | ```
39 |
40 | - Then open your browser at http://localhost:80/.
41 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Sentinel
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/nginx-backend-not-found.conf:
--------------------------------------------------------------------------------
1 | location /api {
2 | return 404;
3 | }
4 | location /docs {
5 | return 404;
6 | }
7 | location /redoc {
8 | return 404;
9 | }
--------------------------------------------------------------------------------
/frontend/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 |
4 | location / {
5 | root /usr/share/nginx/html;
6 | index index.html index.htm;
7 | try_files $uri /index.html =404;
8 | }
9 |
10 | include /etc/nginx/extra-conf.d/*.conf;
11 | }
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@emotion/css": "^11.11.2",
4 | "@emotion/react": "^11.11.4",
5 | "@emotion/styled": "^11.11.5",
6 | "@fontsource/roboto": "^5.0.12",
7 | "@mui/icons-material": "^5.15.7",
8 | "@mui/material": "^5.15.15",
9 | "@mui/x-charts": "^7.1.1",
10 | "@mui/x-data-grid": "^6.19.3",
11 | "axios": "^1.7.4",
12 | "lodash": "^4.17.21",
13 | "nginx": "^1.0.8",
14 | "react-router-dom": "^6.22.0",
15 | "react-virtuoso": "^4.6.3",
16 | "swr": "^2.2.5",
17 | "typescript": "^5.4.5",
18 | "use-local-storage": "^3.0.0",
19 | "vite": "^5.4.6"
20 | },
21 | "scripts": {
22 | "dev": "vite --host 0.0.0.0",
23 | "build": "tsc && vite build",
24 | "preview": "vite preview"
25 | },
26 | "devDependencies": {
27 | "@types/lodash": "^4.17.0",
28 | "@types/react-dom": "^18.2.23",
29 | "@vitejs/plugin-react": "^4.2.1"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | * {
2 | min-width: 0;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | padding: 0;
8 | }
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import { StyledEngineProvider } from "@mui/material";
3 | import { Router } from "./components/Router/Router";
4 |
5 | function App() {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/frontend/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShaharBand/sentinel/351f76b151ba8779b1de2d1a93f9bd2d5215f5bd/frontend/src/assets/images/logo.png
--------------------------------------------------------------------------------
/frontend/src/assets/images/os/android.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShaharBand/sentinel/351f76b151ba8779b1de2d1a93f9bd2d5215f5bd/frontend/src/assets/images/os/android.png
--------------------------------------------------------------------------------
/frontend/src/assets/images/os/ios.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShaharBand/sentinel/351f76b151ba8779b1de2d1a93f9bd2d5215f5bd/frontend/src/assets/images/os/ios.png
--------------------------------------------------------------------------------
/frontend/src/assets/images/os/linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShaharBand/sentinel/351f76b151ba8779b1de2d1a93f9bd2d5215f5bd/frontend/src/assets/images/os/linux.png
--------------------------------------------------------------------------------
/frontend/src/assets/images/os/macos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShaharBand/sentinel/351f76b151ba8779b1de2d1a93f9bd2d5215f5bd/frontend/src/assets/images/os/macos.png
--------------------------------------------------------------------------------
/frontend/src/assets/images/os/windows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShaharBand/sentinel/351f76b151ba8779b1de2d1a93f9bd2d5215f5bd/frontend/src/assets/images/os/windows.png
--------------------------------------------------------------------------------
/frontend/src/components/ContentContainer/ContentContainer.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import { Box, CssBaseline, useTheme } from "@mui/material";
3 | import { getClasses } from "./style";
4 | import { ContentContainerProps } from "./types";
5 |
6 | export const ContentContainer: FC = ({ children }) => {
7 | const theme = useTheme();
8 | const classes = getClasses(theme);
9 |
10 | return (
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/frontend/src/components/ContentContainer/style.ts:
--------------------------------------------------------------------------------
1 | import { css } from "@emotion/css";
2 | import { Theme } from "@mui/material";
3 | import { drawerWidth } from "../NavBar/constants";
4 |
5 | export const getClasses = (theme: Theme) => ({
6 | container: css({
7 | background: theme.palette.primary.main,
8 | marginLeft: `calc(${drawerWidth}px)`.toString(),
9 | padding: "10px",
10 | width: `calc(100% - ${drawerWidth}px)`.toString(),
11 | height: "calc(100vh - 20px - 8rem)",
12 | borderRadius: "0 0 2rem 0",
13 | }),
14 | content: css({
15 | marginLeft: "20px",
16 | marginRight: "20px",
17 | }),
18 | });
19 |
--------------------------------------------------------------------------------
/frontend/src/components/ContentContainer/types.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | export interface ContentContainerProps {
4 | children: ReactNode;
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/src/components/Devices/DeviceAgentTapsCard/DeviceAgentTapsCard.tsx:
--------------------------------------------------------------------------------
1 | import Tabs from "@mui/material/Tabs";
2 | import Tab from "@mui/material/Tab";
3 | import Typography from "@mui/material/Typography";
4 | import Box from "@mui/material/Box";
5 | import { useState } from "react";
6 | import { Card, useTheme } from "@mui/material";
7 | import { getClasses } from "./style";
8 |
9 | interface TabPanelProps {
10 | children?: React.ReactNode;
11 | index: number;
12 | value: number;
13 | }
14 |
15 | function TabPanel(props: TabPanelProps) {
16 | const { children, value, index, ...other } = props;
17 |
18 | return (
19 |
26 | {value === index && (
27 |
28 | {children}
29 |
30 | )}
31 |
32 | );
33 | }
34 |
35 | export default function VerticalTabs() {
36 | const theme = useTheme();
37 | const classes = getClasses(theme);
38 |
39 | const [value, setValue] = useState(0);
40 |
41 | const handleChange = (_event: React.SyntheticEvent, newValue: number) => {
42 | setValue(newValue);
43 | };
44 |
45 | return (
46 |
47 |
56 |
57 |
58 |
59 |
60 | Agent Data Here and Tools activation
61 |
62 |
63 | Terminal
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/frontend/src/components/Devices/DeviceAgentTapsCard/style.ts:
--------------------------------------------------------------------------------
1 | import { css } from "@emotion/css";
2 | import { Theme } from "@mui/material";
3 |
4 | export const getClasses = (theme: Theme) => ({
5 | cardContainer: css({
6 | marginBottom: "20px",
7 | backgroundColor: theme.palette.secondary.main,
8 | borderRadius: "0.5rem",
9 | padding: "10px 20px 10px 20px",
10 | width: "50%",
11 | display: "flex",
12 | height: "50vh",
13 | }),
14 | TabsContainer: css({
15 | "& .MuiTabs-indicator": {
16 | color: theme.palette.accent.main,
17 | backgroundColor: theme.palette.accent.main,
18 | },
19 | "& .Mui-selected": {
20 | color: theme.palette.accent.main,
21 | },
22 | }),
23 | });
24 |
--------------------------------------------------------------------------------
/frontend/src/components/Devices/DeviceCoverageGraph/DeviceCoverageGraph.tsx:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 | import { Card, Stack, Typography, useTheme } from "@mui/material";
3 | import { getClasses } from "./style";
4 | //import { BarChart } from "@mui/x-charts";
5 |
6 | const fetcher = async (url: string, ...args: RequestInit[]): Promise => {
7 | const response = await fetch(url, ...args);
8 | if (!response.ok) {
9 | throw new Error("Network response was not ok");
10 | }
11 | return response.json();
12 | };
13 |
14 | const DeviceCoverageGraph: React.FC = () => {
15 | const theme = useTheme();
16 | const classes = getClasses(theme);
17 |
18 | const {
19 | data: chartData,
20 | error,
21 | isValidating,
22 | } = useSWR(
23 | "http://localhost:5000/api/statistics/devices/security-software-coverage",
24 | fetcher
25 | );
26 |
27 | return (
28 |
29 |
30 | Coverage Distribution
31 |
32 |
33 | {error && <>{error.toString()}>}
34 | {isValidating && <>Loading...>}
35 | {!error && !isValidating && chartData && (
36 | /**/
45 | <>
46 | {Object.entries(chartData).map(([software, coverage], index) => (
47 |
48 | {`${software}: ${coverage}`}
49 |
50 | ))}
51 | >
52 | )}
53 |
54 |
55 | );
56 | };
57 |
58 | export default DeviceCoverageGraph;
59 |
--------------------------------------------------------------------------------
/frontend/src/components/Devices/DeviceCoverageGraph/style.ts:
--------------------------------------------------------------------------------
1 | import { css } from "@emotion/css";
2 | import { Theme } from "@mui/material";
3 |
4 | export const getClasses = (theme: Theme) => ({
5 | chartBox: css({
6 | backgroundColor: theme.palette.secondary.main,
7 | borderRadius: "0.5rem",
8 | height: "calc((80vh - 40px - 8rem)/2)",
9 | marginLeft: "20px",
10 | padding: "10px 20px 10px 20px",
11 | }),
12 | chartTitle: css({}),
13 | chartContainer: css({
14 | display: "flex",
15 | height: "calc((80vh - 80px - 8rem )/2)",
16 | }),
17 | });
18 |
--------------------------------------------------------------------------------
/frontend/src/components/Devices/DeviceCoverageGraph/types.ts:
--------------------------------------------------------------------------------
1 | export interface ChartData {
2 | labels: string[];
3 | values: number[];
4 | }
5 |
--------------------------------------------------------------------------------
/frontend/src/components/Devices/DeviceDesciptionCard/DeviceDesciptionCard.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import { Card, List, Typography, useTheme } from "@mui/material";
3 | import { getClasses } from "./style";
4 | import DeviceTags from "../DeviceTags/DeviceTags";
5 |
6 | export const DeviceDesciptionCard: FC<{}> = ({}) => {
7 | const theme = useTheme();
8 | const classes = getClasses(theme);
9 | return (
10 |
11 |
12 | Description
13 |
14 |
15 |
16 |
17 | test test test test test test test test
18 |
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/frontend/src/components/Devices/DeviceDesciptionCard/style.ts:
--------------------------------------------------------------------------------
1 | import { css } from "@emotion/css";
2 | import { Theme } from "@mui/material";
3 |
4 | export const getClasses = (theme: Theme) => ({
5 | cardContainer: css({
6 | marginBottom: "20px",
7 | backgroundColor: theme.palette.secondary.main,
8 | borderRadius: "0.5rem",
9 | padding: "10px 20px 10px 20px",
10 | height: "calc((80vh -40px - 8rem)/3)",
11 | width: "50%",
12 | }),
13 | cardTitle: css({
14 | marginBottom: "10px",
15 | }),
16 | contentList: css({
17 | marginTop: "10px",
18 | overflowY: "auto",
19 | height: "auto",
20 | }),
21 | contentText: css({
22 | maxHeight: "12vh",
23 | marginBottom: "20px",
24 | }),
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/components/Devices/DeviceNetstatTable/DeviceNetstatTable.tsx:
--------------------------------------------------------------------------------
1 | import { Box, useTheme } from "@mui/material";
2 | import {
3 | Table,
4 | TableBody,
5 | TableCell,
6 | TableContainer,
7 | TableHead,
8 | TableRow,
9 | } from "@mui/material";
10 |
11 | import { getClasses } from "./style";
12 | import { Props } from "./types";
13 |
14 | const DeviceNetstatTable: React.FC = () => {
15 | const theme = useTheme();
16 | const classes = getClasses(theme);
17 |
18 | const data = [
19 | {
20 | Proto: "TCP",
21 | LocalAddress: "0.0.0.0:135",
22 | ForeignAddress: "0.0.0.0:0",
23 | State: "LISTENING",
24 | PID: 1160,
25 | },
26 | {
27 | Proto: "TCP",
28 | LocalAddress: "0.0.0.0:445",
29 | ForeignAddress: "0.0.0.0:0",
30 | State: "LISTENING",
31 | PID: 4,
32 | },
33 | {
34 | Proto: "TCP",
35 | LocalAddress: "0.0.0.0:623",
36 | ForeignAddress: "0.0.0.0:0",
37 | State: "LISTENING",
38 | PID: 4280,
39 | },
40 | {
41 | Proto: "TCP",
42 | LocalAddress: "0.0.0.0:5040",
43 | ForeignAddress: "0.0.0.0:0",
44 | State: "LISTENING",
45 | PID: 7352,
46 | },
47 | {
48 | Proto: "TCP",
49 | LocalAddress: "0.0.0.0:5432",
50 | ForeignAddress: "0.0.0.0:0",
51 | State: "LISTENING",
52 | PID: 5160,
53 | },
54 | {
55 | Proto: "TCP",
56 | LocalAddress: "0.0.0.0:7680",
57 | ForeignAddress: "0.0.0.0:0",
58 | State: "LISTENING",
59 | PID: 9696,
60 | },
61 | {
62 | Proto: "TCP",
63 | LocalAddress: "0.0.0.0:16992",
64 | ForeignAddress: "0.0.0.0:0",
65 | State: "LISTENING",
66 | PID: 4280,
67 | },
68 | {
69 | Proto: "TCP",
70 | LocalAddress: "0.0.0.0:49664",
71 | ForeignAddress: "0.0.0.0:0",
72 | State: "LISTENING",
73 | PID: 984,
74 | },
75 | {
76 | Proto: "TCP",
77 | LocalAddress: "0.0.0.0:49665",
78 | ForeignAddress: "0.0.0.0:0",
79 | State: "LISTENING",
80 | PID: 804,
81 | },
82 | {
83 | Proto: "TCP",
84 | LocalAddress: "0.0.0.0:49666",
85 | ForeignAddress: "0.0.0.0:0",
86 | State: "LISTENING",
87 | PID: 1540,
88 | },
89 | {
90 | Proto: "TCP",
91 | LocalAddress: "0.0.0.0:49667",
92 | ForeignAddress: "0.0.0.0:0",
93 | State: "LISTENING",
94 | PID: 2368,
95 | },
96 | {
97 | Proto: "TCP",
98 | LocalAddress: "0.0.0.0:49668",
99 | ForeignAddress: "0.0.0.0:0",
100 | State: "LISTENING",
101 | PID: 3428,
102 | },
103 | {
104 | Proto: "TCP",
105 | LocalAddress: "0.0.0.0:49693",
106 | ForeignAddress: "0.0.0.0:0",
107 | State: "LISTENING",
108 | PID: 928,
109 | },
110 | {
111 | Proto: "TCP",
112 | LocalAddress: "127.0.0.1:27017",
113 | ForeignAddress: "0.0.0.0:0",
114 | State: "LISTENING",
115 | PID: 4304,
116 | },
117 | {
118 | Proto: "TCP",
119 | LocalAddress: "127.0.0.1:49670",
120 | ForeignAddress: "127.0.0.1:49671",
121 | State: "ESTABLISHED",
122 | PID: 4280,
123 | },
124 | {
125 | Proto: "TCP",
126 | LocalAddress: "127.0.0.1:49671",
127 | ForeignAddress: "127.0.0.1:49670",
128 | State: "ESTABLISHED",
129 | PID: 4280,
130 | },
131 | {
132 | Proto: "TCP",
133 | LocalAddress: "192.168.137.86:139",
134 | ForeignAddress: "0.0.0.0:0",
135 | State: "LISTENING",
136 | PID: 4,
137 | },
138 | ];
139 |
140 | return (
141 |
142 |
143 |