├── .env ├── .gitattributes ├── .github ├── DISCUSSION_TEMPLATE │ └── questions.yml ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── privileged.yml ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── backend ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── pyproject.toml ├── src │ ├── api │ │ ├── app.py │ │ ├── controllers │ │ │ ├── auth.py │ │ │ ├── device.py │ │ │ ├── statistics │ │ │ │ └── device.py │ │ │ └── user.py │ │ ├── models │ │ │ ├── device.py │ │ │ └── user.py │ │ └── routers │ │ │ ├── auth.py │ │ │ ├── device.py │ │ │ ├── statistics │ │ │ ├── __init__.py │ │ │ └── device.py │ │ │ └── user.py │ ├── core │ │ ├── config │ │ │ ├── app.py │ │ │ ├── db.py │ │ │ ├── environment.py │ │ │ ├── metadata.py │ │ │ └── provider.py │ │ ├── db.py │ │ └── security │ │ │ ├── password.py │ │ │ └── token.py │ ├── dal │ │ ├── entities │ │ │ ├── device.py │ │ │ ├── devices │ │ │ │ ├── linux.py │ │ │ │ └── windows.py │ │ │ └── user.py │ │ └── repositories │ │ │ ├── device.py │ │ │ └── user.py │ └── main.py ├── tests │ ├── integration │ │ └── test_devices_integration.py │ └── unit │ │ ├── .env │ │ └── test_users.py └── uv.lock ├── deployment.md ├── docker-compose.yml ├── frontend ├── .dockerignore ├── .eslintrc.cjs ├── .gitignore ├── Dockerfile ├── README.md ├── index.html ├── nginx-backend-not-found.conf ├── nginx.conf ├── package-lock.json ├── package.json ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── images │ │ │ ├── logo.png │ │ │ └── os │ │ │ ├── android.png │ │ │ ├── ios.png │ │ │ ├── linux.png │ │ │ ├── macos.png │ │ │ └── windows.png │ ├── components │ │ ├── ContentContainer │ │ │ ├── ContentContainer.tsx │ │ │ ├── style.ts │ │ │ └── types.ts │ │ ├── Devices │ │ │ ├── DeviceAgentTapsCard │ │ │ │ ├── DeviceAgentTapsCard.tsx │ │ │ │ └── style.ts │ │ │ ├── DeviceCoverageGraph │ │ │ │ ├── DeviceCoverageGraph.tsx │ │ │ │ ├── style.ts │ │ │ │ └── types.ts │ │ │ ├── DeviceDesciptionCard │ │ │ │ ├── DeviceDesciptionCard.tsx │ │ │ │ └── style.ts │ │ │ ├── DeviceNetstatTable │ │ │ │ ├── DeviceNetstatTable.tsx │ │ │ │ ├── style.ts │ │ │ │ └── types.ts │ │ │ ├── DeviceOSCard │ │ │ │ ├── DeviceOSCard.tsx │ │ │ │ └── style.ts │ │ │ ├── DeviceOSPieChart │ │ │ │ ├── DeviceOSPieChart.tsx │ │ │ │ └── style.ts │ │ │ ├── DevicePerformanceCard │ │ │ │ ├── CustomGauge.tsx │ │ │ │ ├── DevicePerformanceCard.tsx │ │ │ │ ├── style.ts │ │ │ │ └── types.ts │ │ │ ├── DeviceTags │ │ │ │ └── DeviceTags.tsx │ │ │ ├── DeviceTapsCard │ │ │ │ ├── DeviceTapsCard.tsx │ │ │ │ └── style.ts │ │ │ ├── DevicesTable │ │ │ │ ├── DevicesTable.tsx │ │ │ │ └── style.ts │ │ │ └── ReportCard │ │ │ │ ├── ReportCard.tsx │ │ │ │ └── style.ts │ │ ├── InfoModal │ │ │ ├── InfoModal.tsx │ │ │ ├── style.ts │ │ │ └── types.ts │ │ ├── NavBar │ │ │ ├── NavBar.tsx │ │ │ ├── constants.ts │ │ │ ├── navItems.tsx │ │ │ └── style.ts │ │ ├── Router │ │ │ ├── Router.tsx │ │ │ ├── constants.tsx │ │ │ └── functions.tsx │ │ ├── StatItem │ │ │ ├── StatItem.tsx │ │ │ ├── style.ts │ │ │ └── types.ts │ │ └── TopBar │ │ │ ├── TopBar.tsx │ │ │ ├── style.ts │ │ │ └── types.ts │ ├── context │ │ └── ThemeContext │ │ │ ├── ThemeContext.tsx │ │ │ ├── darkTheme.ts │ │ │ ├── index.ts │ │ │ ├── lightTheme.ts │ │ │ └── types.ts │ ├── main.tsx │ ├── views │ │ ├── DeviceDetailPage │ │ │ ├── DeviceDetailPage.tsx │ │ │ ├── index.ts │ │ │ └── style.ts │ │ ├── DevicesPage │ │ │ ├── DevicesPage.tsx │ │ │ ├── index.ts │ │ │ └── style.ts │ │ ├── LoginPage │ │ │ ├── LoginPage.tsx │ │ │ └── style.ts │ │ └── NotFound.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── gallery.md ├── img ├── device_details.png ├── devices_dark.png ├── devices_light.png └── logo.png └── scripts └── mongo-setup.sh /.env: -------------------------------------------------------------------------------- 1 | BACKEND_DOMAIN=0.0.0.0 2 | BACKEND_PORT=5000 3 | BACKEND_ENVIRONMENT=production 4 | BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173" 5 | 6 | DB_USER=sentinel 7 | DB_PASSWORD=12345678 8 | DB_PORT=27017 9 | DB_NAME=sentinel 10 | DB_SERVER=localhost -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/questions.yml: -------------------------------------------------------------------------------- 1 | labels: [question] 2 | body: 3 | - type: markdown 4 | attributes: 5 | value: | 6 | Thanks for your interest in this project! 🚀 7 | 8 | Please follow these instructions, fill every question, and do every step. 🙏 9 | 10 | By asking questions in a structured way (following this) it will be much easier to help you. 11 | 12 | And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 13 | 14 | As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 15 | - type: checkboxes 16 | id: checks 17 | attributes: 18 | label: First Check 19 | description: Please confirm and check all the following options. 20 | options: 21 | - label: I added a very descriptive title here. 22 | required: true 23 | - label: I used the GitHub search to find a similar question and didn't find it. 24 | required: true 25 | - label: I searched in the documentation/README. 26 | required: true 27 | - label: I already searched in Google "How to do X" and didn't find any information. 28 | required: true 29 | - label: I already read and followed all the tutorial in the docs/README and didn't find an answer. 30 | required: true 31 | - type: checkboxes 32 | id: help 33 | attributes: 34 | label: Commit to Help 35 | description: | 36 | After submitting this, I commit to one of: 37 | 38 | * Read open questions until I find 2 where I can help someone and add a comment to help there. 39 | * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. 40 | 41 | options: 42 | - label: I commit to help with one of those options 👆 43 | required: true 44 | - type: textarea 45 | id: example 46 | attributes: 47 | label: Example Code 48 | description: | 49 | Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. 50 | 51 | If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. 52 | 53 | placeholder: | 54 | Write your example code here. 55 | render: Text 56 | validations: 57 | required: true 58 | - type: textarea 59 | id: description 60 | attributes: 61 | label: Description 62 | description: | 63 | What is the problem, question, or error? 64 | 65 | Write a short description telling me what you are doing, what you expect to happen, and what is currently happening. 66 | placeholder: | 67 | * Open the browser and call the endpoint `/`. 68 | * It returns a JSON with `{"message": "Hello World"}`. 69 | * But I expected it to return `{"message": "Hello Morty"}`. 70 | validations: 71 | required: true 72 | - type: dropdown 73 | id: os 74 | attributes: 75 | label: Operating System 76 | description: What operating system are you on? 77 | multiple: true 78 | options: 79 | - Linux 80 | - Windows 81 | - macOS 82 | - Other 83 | validations: 84 | required: true 85 | - type: textarea 86 | id: os-details 87 | attributes: 88 | label: Operating System Details 89 | description: You can add more details about your operating system here, in particular if you chose "Other". 90 | validations: 91 | required: true 92 | - type: input 93 | id: python-version 94 | attributes: 95 | label: Python Version 96 | description: | 97 | What Python version are you using? 98 | 99 | You can find the Python version with: 100 | 101 | ```bash 102 | python --version 103 | ``` 104 | validations: 105 | required: true 106 | - type: textarea 107 | id: context 108 | attributes: 109 | label: Additional Context 110 | description: Add any additional context information or screenshots you think are useful. -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [shaharband] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Question or Problem 4 | about: Ask a question or ask about a problem in GitHub Discussions. 5 | url: https://github.com/ShaharBand/sentinel/discussions/categories/questions 6 | - name: Feature Request 7 | about: To suggest an idea or ask about a feature, please start with a question saying what you would like to achieve. There might be a way to do it already. 8 | url: https://github.com/ShaharBand/sentinel/discussions/categories/questions 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/privileged.yml: -------------------------------------------------------------------------------- 1 | name: Privileged 2 | description: You are @shaharband or he asked you directly to create an issue here. If not, check the other options. 👇 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thanks for your interest in this project! 🚀 8 | 9 | If you are not @shaharband or he didn't ask you directly to create an issue here, please start the conversation in a [Question in GitHub Discussions](https://github.com/ShaharBand/sentinel/discussions/categories/questions) instead. 10 | - type: checkboxes 11 | id: privileged 12 | attributes: 13 | label: Privileged issue 14 | description: Confirm that you are allowed to create an issue here. 15 | options: 16 | - label: I'm @shaharband or he asked me directly to create an issue here. 17 | required: true 18 | - type: textarea 19 | id: content 20 | attributes: 21 | label: Issue Content 22 | description: Add the content of the issue here. 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | commit-message: 9 | prefix: ⬆ 10 | # Python 11 | - package-ecosystem: "pip" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | commit-message: 16 | prefix: ⬆ 17 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ "*" ] 6 | pull_request: 7 | branches: [ "*" ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | name: tests 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v3 21 | 22 | - name: Cache Docker layers 23 | uses: actions/cache@v4 24 | with: 25 | path: /tmp/.buildx-cache 26 | key: ${{ runner.os }}-buildx-${{ github.sha }} 27 | restore-keys: | 28 | ${{ runner.os }}-buildx- 29 | 30 | - name: Build Docker image 31 | run: docker build ./backend -t backend-app 32 | 33 | - name: Run linter with ruff 34 | run: docker run --rm backend-app sh -c "cd /backend && ruff check ." 35 | 36 | - name: Run tests with pytest 37 | run: docker run --rm backend-app sh -c "cd /backend && python -m pytest" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Shahar Band 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Sentinel Logo 3 | 4 | 5 | # Sentinel 6 | 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/ShaharBand/Sentinel/blob/main/LICENSE) 8 | [![Downloads](https://img.shields.io/github/downloads/ShaharBand/Sentinel/total.svg)](https://github.com/ShaharBand/Sentinel/releases) 9 | [![GitHub repo size](https://img.shields.io/github/repo-size/ShaharBand/Sentinel.svg)](https://github.com/ShaharBand/Sentinel) 10 | [![stars](https://img.shields.io/github/stars/ShaharBand/Sentinel.svg?style=badge)](https://github.com/ShaharBand/Sentinel/stargazers) 11 | [![Python Version](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) 12 | [![last commit](https://img.shields.io/github/last-commit/ShaharBand/Sentinel.svg)](https://github.com/ShaharBand/Sentinel/commits/main) 13 | [![Tests](https://github.com/ShaharBand/sentinel/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/ShaharBand/sentinel/actions/workflows/tests.yml) 14 |
15 | 16 | A user-friendly Command & Control (C&C) web platform for remote monitoring, management, and task automation across multiple devices. 17 | Equipped with agents, it enables users to seamlessly execute scripted tasks on target devices, empowering efficient data retrieval and remote actions. 18 | 19 |
20 | 21 | ## 🖥️ Technology Stack and Features 22 | 23 | ### Backend 24 | 25 | - ⚡ [**FastAPI**](https://github.com/tiangolo/fastapi): for the Python backend API. 26 | - 🧰 [Beanie](https://github.com/roman-right/beanie): for the Python MongoDB database interactions (ODM). 27 | - 🔍 [Pydantic](https://github.com/samuelcolvin/pydantic): used by FastAPI, for the data validation and settings management. 28 | - 💾 [MongoDB](https://github.com/mongodb/mongo): as the NoSQL database. 29 | - 📦 [UV](https://github.com/astral-sh/uv): An extremely fast Python package and project manager. 30 | 31 | ### Frontend 32 | 33 | - 🚀 [**React**](https://github.com/facebook/react) for the frontend. 34 | - 📜 [TypeScript](https://github.com/microsoft/TypeScript): Enhances JavaScript by adding types. 35 | - ⚡ [Vite](https://github.com/vitejs/vite): A next-generation frontend build tool for a faster and leaner development experience. 36 | - 💅 [EmotionJS](https://github.com/emotion-js/emotion): A library designed for writing CSS styles with JavaScript. 37 | - 🎨 [Material UI](https://github.com/mui/material-ui): for the frontend components. 38 | - 🦇 Dark mode support. 39 | 40 | ### Development and Deployment 41 | 42 | - 🐋 [Docker Compose](https://github.com/docker/compose): for development and production. 43 | - 🔒 Secure password hashing by default. 44 | - 🔑 JWT (JSON Web Token) authentication. 45 | - ✅ Tests with [Pytest](https://github.com/pytest-dev/pytest). 46 | 47 | ### CI/CD 48 | 49 | - 🚢 Deployment instructions using Docker Compose. 50 | - 🏭 CI (continuous integration) and CD (continuous deployment) based on GitHub Actions. 51 | 52 |
53 | 54 | ## 🌱 Getting Started: 55 | 56 | **1. Clone the repository:** 57 | 58 | ```commandline 59 | https://github.com/ShaharBand/Sentinel.git 60 | ``` 61 | 62 |
63 | 64 | **2. Configure** 65 | 66 | You can then update configs in the `.env` files to customize your configurations. 67 | 68 | Before deploying it, make sure you change at least the values for: 69 | 70 | - `DB_USER` 71 | - `DB_PASSWORD` 72 | 73 | You can (and should) pass the database password as environment variable from secrets. 74 | 75 | Read the [deployment.md](deployment.md) docs for more details. 76 | 77 |
78 | 79 | ## Backend Development 80 | 81 | Backend docs: [backend/README.md](backend/README.md). 82 |

83 | 84 | ## Frontend Development 85 | 86 | Frontend docs: [frontend/README.md](frontend/README.md). 87 |

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 | 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 | 144 | 145 | 146 | Proto 147 | 148 | Local Address 149 | 150 | 151 | Foreign Address 152 | 153 | State 154 | PID 155 | 156 | 157 | 158 | {data.map((row, index) => ( 159 | 160 | {row.Proto} 161 | {row.LocalAddress} 162 | {row.ForeignAddress} 163 | {row.State} 164 | {row.PID} 165 | 166 | ))} 167 | 168 |
169 |
170 |
171 | ); 172 | }; 173 | 174 | export default DeviceNetstatTable; 175 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DeviceNetstatTable/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | import { Theme } from "@mui/material"; 3 | 4 | export const getClasses = (theme: Theme) => ({ 5 | contentBox: css({ 6 | display: "flex", 7 | flexDirection: "column", 8 | alignItems: "center", 9 | fontSize: "small", 10 | }), 11 | modalBtn: css({ 12 | marginLeft: "auto", 13 | }), 14 | tableContainer: css({ 15 | overflowY: "auto", 16 | maxHeight: "43vh", 17 | }), 18 | tableHeader: css({ background: theme.palette.primary.light }), 19 | }); 20 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DeviceNetstatTable/types.ts: -------------------------------------------------------------------------------- 1 | export interface RowData { 2 | Proto: string; 3 | LocalAddress: string; 4 | ForeignAddress: string; 5 | State: string; 6 | PID: number; 7 | } 8 | 9 | export interface Props { 10 | data: RowData[]; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DeviceOSCard/DeviceOSCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Stack, Box, Typography, useTheme } from "@mui/material"; 2 | import { getClasses } from "./style"; 3 | import windows from "../../../assets/images/os/windows.png"; 4 | 5 | /* 6 | import ios from "../../../assets/images/os/ios.png"; 7 | import linux from "../../../assets/images/os/linux.png"; 8 | import macos from "../../../assets/images/os/macos.png"; 9 | import android from "../../../assets/images/os/android.png"; 10 | */ 11 | import InfoModal from "../../InfoModal/InfoModal"; 12 | 13 | const DeviceOSCard = () => { 14 | const theme = useTheme(); 15 | const classes = getClasses(theme); 16 | 17 | return ( 18 | 19 | 20 | Operating System 21 | 22 | 71 | 72 | 73 | 74 | 75 | 76 | Microsoft Windows 11 Pro 77 | 10.0.123456 N/A Build 12345 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default DeviceOSCard; 84 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DeviceOSCard/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | import { Theme } from "@mui/material"; 3 | 4 | export const getClasses = (theme: Theme) => ({ 5 | cardBox: css({ 6 | marginBottom: "20px", 7 | backgroundColor: theme.palette.secondary.main, 8 | borderRadius: "0.5rem", 9 | padding: "10px 20px 10px 20px", 10 | width: "25%", 11 | }), 12 | contentBox: css({ 13 | display: "flex", 14 | flexDirection: "column", 15 | alignItems: "center", 16 | justifyContent: "center", 17 | height: "100%", 18 | marginTop: "-20px", 19 | }), 20 | 21 | osImage: css({ 22 | margin: "10px", 23 | objectFit: "contain", 24 | height: "60px", 25 | maxWidth: "70%", 26 | }), 27 | 28 | modalBtn: css({ 29 | marginLeft: "auto", 30 | }), 31 | }); 32 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DeviceOSPieChart/DeviceOSPieChart.tsx: -------------------------------------------------------------------------------- 1 | import { PieChart } from "@mui/x-charts/PieChart"; 2 | import { Card, Typography, useTheme } from "@mui/material"; 3 | import { getClasses } from "./style"; 4 | 5 | const DeviceOSPieChart = () => { 6 | const series = [ 7 | { 8 | data: [ 9 | { name: "Windows", value: 30, label: "Windows" }, 10 | { name: "MacOS", value: 25, label: "MacOS" }, 11 | { name: "Linux", value: 20, label: "Linux" }, 12 | { name: "iOS", value: 15, label: "iOS" }, 13 | { name: "Android", value: 10, label: "Android" }, 14 | ], 15 | innerRadius: "40%", 16 | outerRadius: "80%", 17 | paddingAngle: 5, 18 | cornerRadius: 5, 19 | startAngle: 0, 20 | endAngle: 360, 21 | }, 22 | ]; 23 | const theme = useTheme(); 24 | const classes = getClasses(theme); 25 | 26 | return ( 27 | 28 | 29 | OS Distribution 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default DeviceOSPieChart; 37 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DeviceOSPieChart/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | import { Theme } from "@mui/material"; 3 | 4 | export const getClasses = (theme: Theme) => ({ 5 | chartCard: 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 | marginBottom: "-20px", 14 | }), 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DevicePerformanceCard/CustomGauge.tsx: -------------------------------------------------------------------------------- 1 | import { Gauge, gaugeClasses } from "@mui/x-charts/Gauge"; 2 | import { useTheme } from "@mui/material"; 3 | import { CustomGaugeProps } from "./types"; 4 | 5 | const CustomGauge: React.FC = ({ value }) => { 6 | const theme = useTheme(); 7 | 8 | const getColor = (value: number): string => { 9 | if (value < 50) return theme.palette.success.main; 10 | if (value < 70) return theme.palette.partialError.main; 11 | return theme.palette.error.main; 12 | }; 13 | 14 | return ( 15 | `${value} / ${valueMax}`} 22 | sx={() => ({ 23 | [`& .${gaugeClasses.valueArc}`]: { 24 | fill: getColor(value), 25 | }, 26 | })} 27 | /> 28 | ); 29 | }; 30 | 31 | export default CustomGauge; 32 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DevicePerformanceCard/DevicePerformanceCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Stack, Box, Typography, useTheme } from "@mui/material"; 2 | import { getClasses } from "./style"; 3 | import InfoModal from "../../InfoModal/InfoModal"; 4 | import CustomGauge from "./CustomGauge"; 5 | 6 | const DevicePerformanceCard = () => { 7 | const theme = useTheme(); 8 | const classes = getClasses(theme); 9 | 10 | return ( 11 | 12 | 13 | Performance 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | CPU 29 | 30 | 31 | 32 | 33 | 34 | 35 | Memory 36 | 37 | 38 | 39 | 40 | 41 | 42 | Disk 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Enthernet 51 | 52 | 53 | 54 | 55 | 56 | GPU 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | export default DevicePerformanceCard; 66 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DevicePerformanceCard/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | import { Theme } from "@mui/material"; 3 | 4 | export const getClasses = (theme: Theme) => ({ 5 | cardBox: css({ 6 | width: "25%", 7 | marginBottom: "20px", 8 | backgroundColor: theme.palette.secondary.main, 9 | borderRadius: "0.5rem", 10 | padding: "10px 20px 10px 20px", 11 | }), 12 | contentBox: css({ 13 | display: "flex", 14 | flexDirection: "column", 15 | alignItems: "center", 16 | fontSize: "small", 17 | paddingBottom: "10px", 18 | }), 19 | modalBtn: css({ 20 | marginLeft: "auto", 21 | }), 22 | gaugeBox: css({ 23 | display: "flex", 24 | flexDirection: "column", 25 | alignItems: "center", 26 | justifyContent: "center", 27 | }), 28 | gaugeSubtitle: css({ 29 | marginTop: "-30px", 30 | }), 31 | }); 32 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DevicePerformanceCard/types.ts: -------------------------------------------------------------------------------- 1 | export interface CustomGaugeProps { 2 | value: number; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DeviceTags/DeviceTags.tsx: -------------------------------------------------------------------------------- 1 | import Chip from "@mui/material/Chip"; 2 | import Stack from "@mui/material/Stack"; 3 | 4 | export default function ClickableAndDeletableChips() { 5 | const handleClick = () => { 6 | console.info("You clicked the Chip."); 7 | }; 8 | 9 | const handleDelete = () => { 10 | console.info("You clicked the delete icon."); 11 | }; 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DeviceTapsCard/DeviceTapsCard.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 DeviceNetstatTable from "../DeviceNetstatTable/DeviceNetstatTable"; 6 | import { useState } from "react"; 7 | import { Card, useTheme } from "@mui/material"; 8 | import { getClasses } from "./style"; 9 | 10 | interface TabPanelProps { 11 | children?: React.ReactNode; 12 | index: number; 13 | value: number; 14 | } 15 | 16 | function TabPanel(props: TabPanelProps) { 17 | const { children, value, index, ...other } = props; 18 | 19 | return ( 20 | 33 | ); 34 | } 35 | 36 | export default function VerticalTabs() { 37 | const theme = useTheme(); 38 | const classes = getClasses(theme); 39 | 40 | const [value, setValue] = useState(0); 41 | 42 | const handleChange = (_event: React.SyntheticEvent, newValue: number) => { 43 | setValue(newValue); 44 | }; 45 | 46 | return ( 47 | 48 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Item Two 68 | 69 | 70 | Item Three 71 | 72 | 73 | Item 4 74 | 75 | 76 | Item 5 77 | 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DeviceTapsCard/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/DevicesTable/DevicesTable.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; 3 | import { Link, Card, Stack, Typography, useTheme } from "@mui/material"; 4 | import { getClasses } from "./style"; 5 | 6 | export const DevicesTable: FC<{}> = ({}) => { 7 | const theme = useTheme(); 8 | const classes = getClasses(theme); 9 | 10 | const renderTags = (params: GridRenderCellParams) => ( 11 | 12 | {params.value.map((tag: string, index: number) => ( 13 | 14 | {tag} 15 | 16 | ))} 17 | 18 | ); 19 | 20 | const renderStatusCell = (params: GridRenderCellParams) => { 21 | let cellClass = ""; 22 | switch (params.value.toLowerCase()) { 23 | case "online": 24 | cellClass = classes.online; 25 | break; 26 | case "offline": 27 | cellClass = classes.offline; 28 | break; 29 | case "rejected": 30 | cellClass = classes.rejected; 31 | break; 32 | default: 33 | break; 34 | } 35 | return {params.value}; 36 | }; 37 | 38 | const columns: GridColDef[] = [ 39 | { field: "id", headerName: "ID", flex: 0.5 }, 40 | { field: "name", headerName: "Name", flex: 1 }, 41 | { field: "enforcement", headerName: "Enforcement", flex: 1 }, 42 | { field: "tags", headerName: "Tags", flex: 1.5, renderCell: renderTags }, 43 | { field: "ipAddress", headerName: "IP Address", flex: 1 }, 44 | { field: "operatingSystem", headerName: "Operating System", flex: 1 }, 45 | { field: "agent", headerName: "Agent", flex: 1 }, 46 | { 47 | field: "status", 48 | headerName: "Status", 49 | flex: 1, 50 | renderCell: renderStatusCell, 51 | }, 52 | { field: "lastUpdate", headerName: "Last Update", flex: 1.1 }, 53 | { 54 | field: "actions", 55 | headerName: "Actions", 56 | flex: 0.5, 57 | renderCell: () => ( 58 | 59 | More 60 | 61 | ), 62 | }, 63 | ]; 64 | 65 | const rows = [ 66 | { 67 | id: 1, 68 | name: "Computer A", 69 | enforcement: ["mcaffe", "dns", "dlp"], 70 | tags: ["intel i7", "work pc", "protected", "vpn"], 71 | ipAddress: "198.162.0.1", 72 | operatingSystem: "Windows", 73 | agent: "W 1.0", 74 | status: "Online", 75 | lastUpdate: "11:54, 04/04/2024", 76 | }, 77 | { 78 | id: 2, 79 | name: "Computer B", 80 | enforcement: ["mcaffe", "domain"], 81 | tags: ["intel i7", "work pc", "protected", "vpn", "domain", "1"], 82 | ipAddress: "198.162.0.2", 83 | operatingSystem: "MacOS", 84 | agent: "M 1.0", 85 | status: "Offline", 86 | lastUpdate: "11:54, 04/04/2024", 87 | }, 88 | { 89 | id: 3, 90 | name: "Computer C", 91 | enforcement: ["mcaffe", "dns", "dlp"], 92 | tags: ["intel i5", "test"], 93 | ipAddress: "198.162.0.3", 94 | operatingSystem: "Linux", 95 | agent: "L 1.0", 96 | status: "Online", 97 | lastUpdate: "11:54, 04/04/2024", 98 | }, 99 | { 100 | id: 4, 101 | name: "Computer D", 102 | enforcement: ["domain"], 103 | tags: ["intel i3", "test"], 104 | ipAddress: "198.162.0.4", 105 | operatingSystem: "Windows", 106 | agent: "W 1.0", 107 | status: "Offline", 108 | lastUpdate: "11:54, 04/04/2024", 109 | }, 110 | { 111 | id: 5, 112 | name: "Computer E", 113 | enforcement: ["mcaffe"], 114 | tags: ["intel i7", "test"], 115 | ipAddress: "198.162.0.5", 116 | operatingSystem: "Windows", 117 | agent: "W 1.0", 118 | status: "Online", 119 | lastUpdate: "11:54, 04/04/2024", 120 | }, 121 | { 122 | id: 6, 123 | name: "Computer E", 124 | enforcement: ["mcaffe"], 125 | tags: ["intel i7", "test"], 126 | ipAddress: "198.162.0.6", 127 | operatingSystem: "Windows", 128 | agent: "W 1.0", 129 | status: "Rejected", 130 | lastUpdate: "11:54, 04/04/2024", 131 | }, 132 | { 133 | id: 7, 134 | name: "Computer E", 135 | enforcement: ["mcaffe"], 136 | tags: ["intel i7", "test"], 137 | ipAddress: "198.162.0.7", 138 | operatingSystem: "Windows", 139 | agent: "W 1.0", 140 | status: "Online", 141 | lastUpdate: "11:54, 04/04/2024", 142 | }, 143 | { 144 | id: 8, 145 | name: "Computer E", 146 | enforcement: ["mcaffe"], 147 | tags: ["intel i7", "test"], 148 | ipAddress: "198.162.0.8", 149 | operatingSystem: "Windows", 150 | agent: "W 1.0", 151 | status: "Rejected", 152 | lastUpdate: "11:54, 04/04/2024", 153 | }, 154 | { 155 | id: 9, 156 | name: "Computer E", 157 | enforcement: ["mcaffe"], 158 | tags: ["intel i7", "test"], 159 | ipAddress: "198.162.0.9", 160 | operatingSystem: "Windows", 161 | agent: "W 1.0", 162 | status: "Online", 163 | lastUpdate: "11:54, 04/04/2024", 164 | }, 165 | { 166 | id: 10, 167 | name: "Computer E", 168 | enforcement: ["mcaffe"], 169 | tags: ["intel i7", "test"], 170 | ipAddress: "198.162.0.10", 171 | operatingSystem: "Windows", 172 | agent: "W 1.0", 173 | status: "Online", 174 | lastUpdate: "11:54, 04/04/2024", 175 | }, 176 | { 177 | id: 11, 178 | name: "Computer E", 179 | enforcement: ["mcaffe"], 180 | tags: ["intel i7", "test"], 181 | ipAddress: "198.162.0.11", 182 | operatingSystem: "Windows", 183 | agent: "W 1.0", 184 | status: "Online", 185 | lastUpdate: "11:54, 04/04/2024", 186 | }, 187 | { 188 | id: 12, 189 | name: "Computer E", 190 | enforcement: ["mcaffe"], 191 | tags: ["intel i7", "test"], 192 | ipAddress: "198.162.0.12", 193 | operatingSystem: "Windows", 194 | agent: "W 1.0", 195 | status: "Rejected", 196 | lastUpdate: "11:54, 04/04/2024", 197 | }, 198 | { 199 | id: 13, 200 | name: "Computer E", 201 | enforcement: ["mcaffe"], 202 | tags: ["intel i7", "test"], 203 | ipAddress: "198.162.0.13", 204 | operatingSystem: "Windows", 205 | agent: "W 1.0", 206 | status: "Rejected", 207 | lastUpdate: "11:54, 04/04/2024", 208 | }, 209 | ]; 210 | 211 | return ( 212 | 213 | 214 | 215 | ); 216 | }; 217 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/DevicesTable/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | import { Theme } from "@mui/material"; 3 | 4 | export const getClasses = (theme: Theme) => ({ 5 | tableBox: css({ 6 | backgroundColor: theme.palette.secondary.main, 7 | width: "80%", 8 | borderRadius: "0.5rem", 9 | height: "calc(80vh - 20px - 8rem)", 10 | }), 11 | dataGrid: css({ 12 | border: 0, 13 | overflow: "hidden !important", 14 | 15 | "& .MuiDataGrid-cell:hover": { 16 | color: theme.palette.accent.main, 17 | cursor: "pointer", 18 | }, 19 | }), 20 | tagStack: css({ 21 | justifyContent: "flex-start", 22 | whiteSpace: "wrap", 23 | }), 24 | tag: css({ 25 | backgroundColor: theme.palette.primary.main, 26 | color: theme.palette.primary.contrastText, 27 | marginRight: "5px", 28 | marginTop: "5px", 29 | padding: "2.5px 10px", 30 | borderRadius: "1rem", 31 | }), 32 | 33 | online: css({ 34 | backgroundColor: theme.palette.primary.main, 35 | color: theme.palette.success.main, 36 | padding: "2.5px 10px", 37 | borderRadius: "1rem", 38 | }), 39 | offline: css({ 40 | backgroundColor: theme.palette.primary.main, 41 | color: theme.palette.error.main, 42 | padding: "2.5px 10px", 43 | borderRadius: "1rem", 44 | }), 45 | rejected: css({ 46 | backgroundColor: theme.palette.primary.main, 47 | color: theme.palette.partialError.main, 48 | padding: "2.5px 10px", 49 | borderRadius: "1rem", 50 | }), 51 | link: css({ 52 | color: theme.palette.accent.main, 53 | }), 54 | }); 55 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/ReportCard/ReportCard.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Box, Button, Card, Stack, Typography, useTheme } from "@mui/material"; 3 | import { getClasses } from "./style"; 4 | 5 | import SummarizeIcon from "@mui/icons-material/Summarize"; 6 | import WarningAmberIcon from "@mui/icons-material/WarningAmber"; 7 | 8 | export const ReportCard: FC<{}> = ({}) => { 9 | const theme = useTheme(); 10 | const classes = getClasses(theme); 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Devices Database Report 20 | Today - April 15, 2023 21 | 22 | 23 | 24 | 25 | there are 21 new devices recorded. 26 | 27 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/src/components/Devices/ReportCard/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 | display: "flex", 7 | justifyContent: "space-between", 8 | whiteSpace: "nowrap", 9 | alignItems: "center", 10 | gap: "10px", 11 | marginBottom: "20px", 12 | backgroundColor: theme.palette.secondary.main, 13 | borderRadius: "0.5rem", 14 | }), 15 | iconBox: css({ 16 | padding: "15px", 17 | display: "flex", 18 | alignItems: "center", 19 | justifyContent: "center", 20 | "& *": { 21 | width: "30px", 22 | height: "30px", 23 | }, 24 | }), 25 | labelStack: css({ 26 | justifyContent: "space-between", 27 | whiteSpace: "nowrap", 28 | }), 29 | iconWarning: css({ 30 | color: theme.palette.accent.main, 31 | marginRight: "10px", 32 | }), 33 | reportButton: css({ 34 | marginRight: "15px", 35 | backgroundColor: theme.palette.accent.main, 36 | color: theme.palette.accent.contrastText, 37 | transition: "scale ease-in-out 0.2s", 38 | 39 | ":hover": { 40 | backgroundColor: theme.palette.accent.dark, 41 | scale: "1.02", 42 | }, 43 | }), 44 | }); 45 | -------------------------------------------------------------------------------- /frontend/src/components/InfoModal/InfoModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Card, 4 | IconButton, 5 | Modal, 6 | Tooltip, 7 | Typography, 8 | useTheme, 9 | } from "@mui/material"; 10 | import { useState } from "react"; 11 | import InfoIcon from "@mui/icons-material/Info"; 12 | import { getClasses } from "./style"; 13 | import { InfoModalProps } from "./types"; 14 | 15 | const InfoModal: React.FC = ({ title, content, tooltip }) => { 16 | const theme = useTheme(); 17 | const classes = getClasses(theme); 18 | 19 | const [open, setOpen] = useState(false); 20 | const handleOpen = () => setOpen(true); 21 | const handleClose = () => setOpen(false); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 37 | 38 | 39 | {title} 40 | 41 | 42 |
{content}
43 |
44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default InfoModal; 51 | -------------------------------------------------------------------------------- /frontend/src/components/InfoModal/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | import { Theme } from "@mui/material"; 3 | 4 | export const getClasses = (theme: Theme) => ({ 5 | modalCard: css({ 6 | position: "absolute", 7 | top: "50%", 8 | left: "50%", 9 | transform: "translate(-50%, -50%)", 10 | background: theme.palette.background.paper, 11 | padding: "10px 20px", 12 | maxHeight: "80vh", 13 | overflowY: "auto", 14 | }), 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/components/InfoModal/types.ts: -------------------------------------------------------------------------------- 1 | export interface InfoModalProps { 2 | title: string; 3 | content: string; 4 | tooltip: string; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer, useTheme } from "@mui/material"; 2 | import Toolbar from "@mui/material/Toolbar"; 3 | import List from "@mui/material/List"; 4 | import Typography from "@mui/material/Typography"; 5 | import ListItem from "@mui/material/ListItem"; 6 | import ListItemButton from "@mui/material/ListItemButton"; 7 | import ListItemIcon from "@mui/material/ListItemIcon"; 8 | import ListItemText from "@mui/material/ListItemText"; 9 | import { getClasses } from "./style"; 10 | import { FC } from "react"; 11 | import logo from "../../assets/images/logo.png"; 12 | import { HEADLINE } from "./constants"; 13 | import { bottomNavItems, navItems } from "./navItems"; 14 | 15 | export const NavBar: FC<{}> = ({}) => { 16 | const theme = useTheme(); 17 | const classes = getClasses(theme); 18 | return ( 19 | 25 | 26 | 27 | {HEADLINE} 28 | 29 | 30 | 31 | {navItems.map((item, index) => ( 32 | 33 | 34 | {item.icon} 35 | 36 | 37 | 38 | ))} 39 | 40 | 41 | {bottomNavItems.map((item, index) => ( 42 | 43 | 44 | {item.icon} 45 | 46 | 47 | 48 | ))} 49 | 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar/constants.ts: -------------------------------------------------------------------------------- 1 | export const HEADLINE: string = "SENTINEL"; 2 | export const drawerWidth: number = 240; 3 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar/navItems.tsx: -------------------------------------------------------------------------------- 1 | import DevicesRoundedIcon from "@mui/icons-material/DevicesRounded"; 2 | import SmartToyRoundedIcon from "@mui/icons-material/SmartToyRounded"; 3 | import PendingActionsRoundedIcon from "@mui/icons-material/PendingActionsRounded"; 4 | import SettingsRoundedIcon from "@mui/icons-material/SettingsRounded"; 5 | import ExitToAppIcon from "@mui/icons-material/ExitToApp"; 6 | 7 | export const navItems = [ 8 | { 9 | text: "Devices", 10 | icon: , 11 | url: "/devices", 12 | }, 13 | { 14 | text: "Agents", 15 | icon: , 16 | url: "/agents", 17 | }, 18 | { 19 | text: "Actions", 20 | icon: , 21 | url: "/actions", 22 | }, 23 | { 24 | text: "Settings", 25 | icon: , 26 | url: "/settings", 27 | }, 28 | ]; 29 | 30 | export const bottomNavItems = [ 31 | { 32 | text: "Logout", 33 | icon: , 34 | url: "/logout", 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | import { Theme } from "@mui/material"; 3 | import { drawerWidth } from "./constants"; 4 | 5 | export const getClasses = (theme: Theme) => ({ 6 | navDrawer: css({}), 7 | 8 | drawerPaper: css({ 9 | margin: "10px", 10 | maxHeight: "calc(100% - 20px)", 11 | width: `${drawerWidth}px`.toString(), 12 | borderRadius: "2rem 0 0 2rem", 13 | backgroundColor: theme.palette.primary.main, 14 | }), 15 | 16 | logoImage: css({ 17 | width: "30px", 18 | marginRight: "20px", 19 | }), 20 | logoBar: css({ 21 | height: "8rem", 22 | lineHeight: "30px", 23 | paddingLeft: "30px", 24 | }), 25 | headline: css({ 26 | textTransform: "uppercase", 27 | letterSpacing: "3px", 28 | fontSize: "1.25rem", 29 | fontWeight: "500", 30 | }), 31 | bottomList: css({ 32 | position: "fixed", 33 | bottom: "30px", 34 | width: "inherit", 35 | }), 36 | navItem: css({ 37 | borderRadius: "1rem", 38 | }), 39 | }); 40 | -------------------------------------------------------------------------------- /frontend/src/components/Router/Router.tsx: -------------------------------------------------------------------------------- 1 | import { map, values } from "lodash/fp"; 2 | import { FC } from "react"; 3 | import { BrowserRouter, Route, Routes } from "react-router-dom"; 4 | import { createRoute } from "./functions"; 5 | import { PAGES } from "./constants"; 6 | import NotFound from "../../views/NotFound"; 7 | 8 | export const Router: FC = () => { 9 | const routes = map(createRoute, values(PAGES)); 10 | 11 | return ( 12 | 13 | 14 | {routes} 15 | } /> 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/components/Router/constants.tsx: -------------------------------------------------------------------------------- 1 | import { DeviceDetailPage } from "../../views/DeviceDetailPage"; 2 | import { DevicesPage } from "../../views/DevicesPage"; 3 | import { LoginPage } from "../../views/LoginPage/LoginPage"; 4 | 5 | export const PAGES = { 6 | home: { 7 | path: "/", 8 | element: , 9 | }, 10 | devices: { 11 | path: "devices/", 12 | element: , 13 | }, 14 | agents: { 15 | path: "agents/", 16 | element: , 17 | }, 18 | actions: { 19 | path: "actions/", 20 | element: , 21 | }, 22 | settings: { 23 | path: "settings/", 24 | element: , 25 | }, 26 | deviceDetail: { 27 | path: "devices/:id", 28 | element: , 29 | }, 30 | login: { 31 | path: "login/", 32 | element: , 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /frontend/src/components/Router/functions.tsx: -------------------------------------------------------------------------------- 1 | import { Route } from "react-router-dom"; 2 | 3 | export const createRoute = ({ 4 | path, 5 | element, 6 | }: { 7 | path: string; 8 | element: JSX.Element; 9 | }): JSX.Element => ; 10 | -------------------------------------------------------------------------------- /frontend/src/components/StatItem/StatItem.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Card, Stack, Tooltip, Typography, useTheme } from "@mui/material"; 3 | import { getClasses } from "./style"; 4 | import { StatItemProps } from "./types"; 5 | 6 | import TrendingUpIcon from "@mui/icons-material/TrendingUp"; 7 | import TrendingDownIcon from "@mui/icons-material/TrendingDown"; 8 | 9 | export const StatItem: FC = ({ 10 | icon, 11 | title, 12 | value, 13 | description, 14 | offset, 15 | }) => { 16 | const theme = useTheme(); 17 | const classes = getClasses(theme); 18 | return ( 19 | 20 | {icon} 21 | 22 | 23 | 24 | {title} 25 | 26 | 27 | {value} 28 | 29 | {offset !== 0 && 30 | (offset > 0 ? ( 31 | {offset}% 32 | ) : ( 33 | {offset}% 34 | ))} 35 | 36 | {offset !== 0 && 37 | (offset > 0 ? ( 38 | 39 | ) : ( 40 | 41 | ))} 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /frontend/src/components/StatItem/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | import { Theme } from "@mui/material"; 3 | 4 | export const getClasses = (theme: Theme) => ({ 5 | stackContainer: css({ 6 | display: "flex", 7 | alignItems: "center", 8 | gap: "10px", 9 | marginBottom: "20px", 10 | marginRight: "5%", 11 | }), 12 | iconCard: css({ 13 | backgroundColor: theme.palette.secondary.main, 14 | padding: "15px", 15 | display: "flex", 16 | alignItems: "center", 17 | justifyContent: "center", 18 | borderRadius: "0.5rem", 19 | "& *": { 20 | width: "30px", 21 | height: "30px", 22 | }, 23 | }), 24 | contentStack: css({ 25 | height: "60px", 26 | display: "flex", 27 | justifyContent: "space-between", 28 | }), 29 | statStack: css({ 30 | display: "flex", 31 | justifyContent: "space-between", 32 | }), 33 | 34 | greenIcon: css({ 35 | color: theme.palette.success.main, 36 | }), 37 | redIcon: css({ 38 | color: theme.palette.error.main, 39 | }), 40 | }); 41 | -------------------------------------------------------------------------------- /frontend/src/components/StatItem/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface StatItemProps { 4 | icon: ReactNode; 5 | title: string; 6 | value: string | number; 7 | description: string; 8 | offset: number; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/components/TopBar/TopBar.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useContext } from "react"; 2 | import { getClasses } from "./style"; 3 | import AppBar from "@mui/material/AppBar"; 4 | import Toolbar from "@mui/material/Toolbar"; 5 | import { 6 | Box, 7 | IconButton, 8 | Stack, 9 | ToggleButton, 10 | Typography, 11 | useTheme, 12 | } from "@mui/material"; 13 | import PersonIcon from "@mui/icons-material/Person"; 14 | import LightModeIcon from "@mui/icons-material/LightMode"; 15 | import DarkModeIcon from "@mui/icons-material/DarkMode"; 16 | import { ColorModeContext } from "../../context/ThemeContext/ThemeContext"; 17 | import { TopBarProps } from "./types"; 18 | 19 | export const TopBar: FC = ({ title }) => { 20 | const theme = useTheme(); 21 | const classes = getClasses(theme); 22 | 23 | const { toggleColorMode, mode } = useContext(ColorModeContext); 24 | const handleToggleColorMode = () => { 25 | toggleColorMode(); 26 | }; 27 | 28 | return ( 29 | 30 | 31 | 32 | {title} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 46 | 52 | {mode === "light" ? ( 53 | 54 | ) : ( 55 | 56 | )} 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /frontend/src/components/TopBar/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 | topBar: css({ 7 | width: `calc(100% - ${drawerWidth}px)`.toString(), 8 | marginLeft: `calc(${drawerWidth}px)`.toString(), 9 | position: "relative", 10 | borderRadius: "0 20px 0 0", 11 | background: theme.palette.primary.main, 12 | }), 13 | rightSideStack: css({ 14 | position: "fixed", 15 | alignItems: "center", 16 | right: "40px", 17 | gap: "10px", 18 | }), 19 | content: css({ 20 | marginLeft: "20px", 21 | height: "8rem", 22 | lineHeight: "30px", 23 | }), 24 | title: css({ 25 | letterSpacing: "1px", 26 | fontSize: "1.6rem", 27 | fontWeight: "500", 28 | }), 29 | ButtonIcon: css({ 30 | color: theme.palette.primary.contrastText, 31 | border: "none", 32 | }), 33 | buttonWrapper: css({ 34 | border: "1px solid", 35 | borderRadius: "0.5rem", 36 | height: "45px", 37 | width: "45px", 38 | display: "flex", 39 | alignItems: "center", 40 | justifyContent: "center", 41 | }), 42 | }); 43 | -------------------------------------------------------------------------------- /frontend/src/components/TopBar/types.ts: -------------------------------------------------------------------------------- 1 | export interface TopBarProps { 2 | title: string; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/context/ThemeContext/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, FC, useMemo } from "react"; 2 | import { createTheme, ThemeProvider } from "@mui/material/styles"; 3 | import { GlobalStyles, PaletteMode } from "@mui/material"; 4 | import useLocalStorage from "use-local-storage"; 5 | import { ContextProps } from "./types"; 6 | import { darkTheme } from "./darkTheme"; 7 | import { lightTheme } from "./lightTheme"; 8 | import "@fontsource/roboto"; 9 | 10 | export const ColorModeContext = createContext({ 11 | toggleColorMode: () => {}, 12 | mode: "dark", 13 | }); 14 | 15 | export const ThemeContext: FC = ({ children }) => { 16 | const [mode, setMode] = useLocalStorage("colorMode", "dark"); 17 | 18 | const toggleColorMode = () => 19 | setMode((prevMode: PaletteMode | undefined) => 20 | prevMode === "light" ? "dark" : "light" 21 | ); 22 | 23 | const { palette } = createTheme(); 24 | 25 | const theme = useMemo( 26 | () => 27 | createTheme({ 28 | palette: { 29 | mode, 30 | ...(mode === "light" ? lightTheme : darkTheme), 31 | }, 32 | typography: { 33 | fontFamily: "Roboto", 34 | fontWeightMedium: 600, 35 | }, 36 | }), 37 | [mode, palette.grey] 38 | ); 39 | return ( 40 | 41 | 58 | 59 | {children} 60 | 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /frontend/src/context/ThemeContext/darkTheme.ts: -------------------------------------------------------------------------------- 1 | import { ThemePalette } from "./types"; 2 | 3 | export const darkTheme: ThemePalette = { 4 | primary: { 5 | main: "#22222a", 6 | contrastText: "#FEFFFF", 7 | }, 8 | secondary: { 9 | main: "#37373e", 10 | contrastText: "#FEFFFF", 11 | }, 12 | accent: { 13 | main: "#a7a7ff", 14 | dark: "#9191FF", 15 | contrastText: "#FFFFFF", 16 | }, 17 | text: { 18 | primary: "#FEFFFF", 19 | secondary: "#a2a2a2", 20 | }, 21 | background: { 22 | default: "#888d99", 23 | }, 24 | error: { 25 | main: "#ef233c", 26 | }, 27 | partialError: { 28 | main: "#FFA500", 29 | }, 30 | valid: { 31 | main: "#06d6a0", 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/src/context/ThemeContext/index.ts: -------------------------------------------------------------------------------- 1 | import { ThemeContext } from "./ThemeContext"; 2 | 3 | export default ThemeContext; 4 | -------------------------------------------------------------------------------- /frontend/src/context/ThemeContext/lightTheme.ts: -------------------------------------------------------------------------------- 1 | import { ThemePalette } from "./types"; 2 | 3 | export const lightTheme: ThemePalette = { 4 | primary: { 5 | main: "#e4e5f1", 6 | contrastText: "#000000", 7 | }, 8 | secondary: { 9 | main: "#F4F4FF", 10 | contrastText: "#ffffff", 11 | }, 12 | accent: { 13 | main: "#a7a7ff", 14 | dark: "#9191FF", 15 | contrastText: "#FFFFFF", 16 | }, 17 | text: { 18 | primary: "#000000", 19 | secondary: "#121212", 20 | }, 21 | background: { 22 | default: "#888d99", 23 | }, 24 | error: { 25 | main: "#ef233c", 26 | }, 27 | partialError: { 28 | main: "#FFA500", 29 | }, 30 | valid: { 31 | main: "#06d6a0", 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/src/context/ThemeContext/types.ts: -------------------------------------------------------------------------------- 1 | declare module "@mui/material/styles" { 2 | // eslint-disable-next-line 3 | interface PaletteOptions extends ThemePalette {} 4 | 5 | // eslint-disable-next-line 6 | interface Palette extends ThemePalette {} 7 | } 8 | 9 | export interface ThemePalette { 10 | primary: { 11 | main: string; 12 | contrastText: string; 13 | }; 14 | secondary: { 15 | main: string; 16 | contrastText: string; 17 | }; 18 | accent: { 19 | main: string; 20 | dark: string; 21 | contrastText: string; 22 | }; 23 | text: { 24 | primary: string; 25 | secondary: string; 26 | }; 27 | background: { 28 | default: string; 29 | }; 30 | error: { 31 | main: string; 32 | }; 33 | partialError: { 34 | main: string; 35 | }; 36 | valid: { 37 | main: string; 38 | }; 39 | } 40 | 41 | export interface ContextProps { 42 | children: JSX.Element[] | JSX.Element; 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import { ThemeContext } from "./context/ThemeContext/ThemeContext"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /frontend/src/views/DeviceDetailPage/DeviceDetailPage.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Box, Stack } from "@mui/material"; 3 | import { getClasses } from "./style"; 4 | 5 | import { NavBar } from "../../components/NavBar/NavBar"; 6 | import { TopBar } from "../../components/TopBar/TopBar"; 7 | import { ContentContainer } from "../../components/ContentContainer/ContentContainer"; 8 | import { DeviceDesciptionCard } from "../../components/Devices/DeviceDesciptionCard/DeviceDesciptionCard"; 9 | import DeviceOSCard from "../../components/Devices/DeviceOSCard/DeviceOSCard"; 10 | import DevicePerformanceCard from "../../components/Devices/DevicePerformanceCard/DevicePerformanceCard"; 11 | import DeviceTapsCard from "../../components/Devices/DeviceTapsCard/DeviceTapsCard"; 12 | import DeviceAgentTapsCard from "../../components/Devices/DeviceAgentTapsCard/DeviceAgentTapsCard"; 13 | 14 | export const DeviceDetailPage: FC = () => { 15 | const classes = getClasses(); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /frontend/src/views/DeviceDetailPage/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./DeviceDetailPage"; 2 | -------------------------------------------------------------------------------- /frontend/src/views/DeviceDetailPage/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | 3 | export const getClasses = () => ({ 4 | container: css({ 5 | position: "relative", 6 | overflowY: "hidden", 7 | margin: "10px", 8 | }), 9 | layoutStack: css({ 10 | gap: "20px", 11 | height: "100%", 12 | }), 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/src/views/DevicesPage/DevicesPage.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Box, Stack } from "@mui/material"; 3 | import { getClasses } from "./style"; 4 | 5 | import { NavBar } from "../../components/NavBar/NavBar"; 6 | import { TopBar } from "../../components/TopBar/TopBar"; 7 | import { ContentContainer } from "../../components/ContentContainer/ContentContainer"; 8 | import { StatItem } from "../../components/StatItem/StatItem"; 9 | 10 | import DevicesRoundedIcon from "@mui/icons-material/DevicesRounded"; 11 | import NetworkPingRoundedIcon from "@mui/icons-material/NetworkPingRounded"; 12 | import LeakAddRoundedIcon from "@mui/icons-material/LeakAddRounded"; 13 | import SmartToyRoundedIcon from "@mui/icons-material/SmartToyRounded"; 14 | import TaskIcon from "@mui/icons-material/Task"; 15 | import { ReportCard } from "../../components/Devices/ReportCard/ReportCard"; 16 | import { DevicesTable } from "../../components/Devices/DevicesTable/DevicesTable"; 17 | import DeviceOSPieChart from "../../components/Devices/DeviceOSPieChart/DeviceOSPieChart"; 18 | import DeviceCoverageGraph from "../../components/Devices/DeviceCoverageGraph/DeviceCoverageGraph"; 19 | 20 | export const DevicesPage: FC = () => { 21 | const classes = getClasses(); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | } 31 | title="Total Devices" 32 | value={10} 33 | description="The total amount of unique records in the devices database" 34 | offset={0} 35 | /> 36 | } 38 | title="Reachable Devices" 39 | value={10} 40 | description="The total amount of devices that were pingable in the last scan" 41 | offset={-1.2} 42 | /> 43 | } 45 | title="Communicating Devices" 46 | value={10} 47 | description="The total amount of devices that were communicating through there agent in the last scan" 48 | offset={7.6} 49 | /> 50 | } 52 | title="Total Agents" 53 | value={10} 54 | description="The total types of agents deployed in the current devices recorded in the database" 55 | offset={11} 56 | /> 57 | } 59 | title="Last Complete Scan" 60 | value={"03/04/2024"} 61 | description="The last date we scanned the entire device list records from the database" 62 | offset={0} 63 | /> 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /frontend/src/views/DevicesPage/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./DevicesPage"; 2 | -------------------------------------------------------------------------------- /frontend/src/views/DevicesPage/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | 3 | export const getClasses = () => ({ 4 | container: css({ 5 | position: "relative", 6 | overflowY: "hidden", 7 | margin: "10px", 8 | }), 9 | graphsContainer: css({ 10 | width: "20%", 11 | height: "100%", 12 | gap: "20px", 13 | }), 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/src/views/LoginPage/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState, useEffect } from "react"; 2 | import { Box, Button, useTheme } from "@mui/material"; 3 | import { getClasses } from "./style"; 4 | import axios from "axios"; 5 | 6 | interface PersonData { 7 | results: any[]; 8 | } 9 | 10 | export const LoginPage: FC = () => { 11 | const theme = useTheme(); 12 | const classes = getClasses(theme); 13 | 14 | const [counter, setCounter] = useState(0); 15 | const [currentPage, setCurrentPage] = useState(1); 16 | const [personData, setPersonData] = useState(null); 17 | 18 | useEffect(() => { 19 | fetchUsersData(currentPage); 20 | }, []); 21 | 22 | const fetchUsersData = (page: number) => { 23 | axios.get(`https://randomuser.me/api?page=${page}`) 24 | .then((response) => { 25 | setPersonData(response.data); 26 | setCurrentPage(page); 27 | }) 28 | .catch(() => { 29 | setPersonData(null); 30 | }); 31 | }; 32 | 33 | const handleNextPage = () => { 34 | fetchUsersData(currentPage + 1); 35 | }; 36 | 37 | const handlePrevPage = () => { 38 | if (currentPage > 1) { 39 | fetchUsersData(currentPage - 1); 40 | } 41 | }; 42 | 43 | return ( 44 | <> 45 | {/* counter code */} 46 | 47 |

{counter}

48 | 49 |
50 | {/* fetch code */} 51 | 52 |

Current Page: {currentPage}

53 |

54 | {personData && JSON.stringify(personData.results)} 55 |

56 | 57 | 58 |
59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /frontend/src/views/LoginPage/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "@emotion/css"; 2 | import { Theme } from "@mui/material"; 3 | 4 | export const getClasses = (theme: Theme) => ({ 5 | container: css({ 6 | width: "1000px", 7 | background: theme.palette.background.default, 8 | color: "red", 9 | }), 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/src/views/NotFound.tsx: -------------------------------------------------------------------------------- 1 | const NotFound = () => { 2 | return <>404; 3 | }; 4 | 5 | export default NotFound; 6 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /gallery.md: -------------------------------------------------------------------------------- 1 | # Dashboard - Devices 2 | 3 |
4 | Devices Dashboard Dark Theme 5 |
6 | 7 | 8 | # Dashboard - Device Details 9 | 10 |
11 | Device Details Dashboard Dark Theme 12 |
13 | 14 | # Dashboard - Light Mode 15 | 16 |
17 | Devices Dashboard Light Theme 18 |
19 | 20 | -------------------------------------------------------------------------------- /img/device_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaharBand/sentinel/351f76b151ba8779b1de2d1a93f9bd2d5215f5bd/img/device_details.png -------------------------------------------------------------------------------- /img/devices_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaharBand/sentinel/351f76b151ba8779b1de2d1a93f9bd2d5215f5bd/img/devices_dark.png -------------------------------------------------------------------------------- /img/devices_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaharBand/sentinel/351f76b151ba8779b1de2d1a93f9bd2d5215f5bd/img/devices_light.png -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaharBand/sentinel/351f76b151ba8779b1de2d1a93f9bd2d5215f5bd/img/logo.png -------------------------------------------------------------------------------- /scripts/mongo-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$DB_USER" ] || [ -z "$DB_PASSWORD" ] || [ -z "$DB_SERVER" ] || [ -z "$DB_PORT" ] || [ -z "$DB_NAME" ]; then 4 | echo "Error: Please make sure all required environment variables are set." 5 | exit 1 6 | fi 7 | 8 | mongosh --host $DB_SERVER --port $DB_PORT --authenticationDatabase admin <