mirror of
https://github.com/mblanke/ThreatHunt.git
synced 2026-03-01 14:00:20 -05:00
this is the first commit for the Claude Iteration project.
This commit is contained in:
3
.env
Normal file
3
.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
DATABASE_URL=postgresql://admin:secure_password_123@database:5432/threat_hunter
|
||||||
|
SECRET_KEY=your-very-secret-key-change-in-production
|
||||||
|
FLASK_ENV=production
|
||||||
23
backend/Dockerfile
Normal file
23
backend/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
postgresql-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
RUN mkdir -p uploads output
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["python", "app.py"]
|
||||||
24
backend/Dockerfile.prod
Normal file
24
backend/Dockerfile.prod
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
postgresql-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN useradd --create-home --shell /bin/bash app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN chown -R app:app /app
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]
|
||||||
303
backend/app.py
303
backend/app.py
@@ -1,9 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from flask import Flask, request, jsonify, send_from_directory
|
from flask import Flask, request, jsonify, send_from_directory
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from werkzeug.exceptions import RequestEntityTooLarge
|
import bcrypt
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Try to import flask-cors
|
# Try to import flask-cors
|
||||||
try:
|
try:
|
||||||
@@ -19,6 +21,24 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
app = Flask(__name__, static_folder="../frontend/dist")
|
app = Flask(__name__, static_folder="../frontend/dist")
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'postgresql://admin:secure_password_123@localhost:5432/threat_hunter')
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
app.config['JWT_SECRET_KEY'] = os.getenv('SECRET_KEY', 'change-this-in-production')
|
||||||
|
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=24)
|
||||||
|
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
|
||||||
|
app.config['UPLOAD_FOLDER'] = 'uploaded'
|
||||||
|
app.config['OUTPUT_FOLDER'] = 'output'
|
||||||
|
app.config['ALLOWED_EXTENSIONS'] = {'csv', 'json', 'txt', 'log'}
|
||||||
|
|
||||||
|
# Ensure upload directories exist
|
||||||
|
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||||
|
os.makedirs(app.config['OUTPUT_FOLDER'], exist_ok=True)
|
||||||
|
|
||||||
|
# Initialize extensions
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
jwt = JWTManager(app)
|
||||||
|
|
||||||
# Enable CORS
|
# Enable CORS
|
||||||
if CORS_AVAILABLE:
|
if CORS_AVAILABLE:
|
||||||
CORS(app)
|
CORS(app)
|
||||||
@@ -30,16 +50,6 @@ else:
|
|||||||
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
|
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Configuration
|
|
||||||
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
|
|
||||||
app.config['UPLOAD_FOLDER'] = 'uploaded'
|
|
||||||
app.config['OUTPUT_FOLDER'] = 'output'
|
|
||||||
app.config['ALLOWED_EXTENSIONS'] = {'csv', 'json', 'txt', 'log'}
|
|
||||||
|
|
||||||
# Ensure upload directories exist
|
|
||||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
|
||||||
os.makedirs(app.config['OUTPUT_FOLDER'], exist_ok=True)
|
|
||||||
|
|
||||||
def allowed_file(filename):
|
def allowed_file(filename):
|
||||||
return '.' in filename and \
|
return '.' in filename and \
|
||||||
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
||||||
@@ -53,6 +63,144 @@ def handle_exception(e):
|
|||||||
logger.error(f"Unhandled exception: {e}")
|
logger.error(f"Unhandled exception: {e}")
|
||||||
return jsonify({'error': 'Internal server error'}), 500
|
return jsonify({'error': 'Internal server error'}), 500
|
||||||
|
|
||||||
|
# Database Models
|
||||||
|
class User(db.Model):
|
||||||
|
__tablename__ = 'users'
|
||||||
|
id = db.Column(db.String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||||
|
username = db.Column(db.String(50), unique=True, nullable=False)
|
||||||
|
email = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
last_login = db.Column(db.DateTime)
|
||||||
|
|
||||||
|
class Hunt(db.Model):
|
||||||
|
__tablename__ = 'hunts'
|
||||||
|
id = db.Column(db.String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||||
|
name = db.Column(db.String(100), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
created_by = db.Column(db.String, db.ForeignKey('users.id'))
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
status = db.Column(db.String(20), default='active')
|
||||||
|
|
||||||
|
# Authentication Routes
|
||||||
|
@app.route('/api/auth/login', methods=['POST'])
|
||||||
|
def login():
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
|
||||||
|
if user and bcrypt.checkpw(password.encode('utf-8'), user.password_hash.encode('utf-8')):
|
||||||
|
access_token = create_access_token(identity=user.id)
|
||||||
|
user.last_login = datetime.utcnow()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'access_token': access_token,
|
||||||
|
'user': {
|
||||||
|
'id': user.id,
|
||||||
|
'username': user.username,
|
||||||
|
'email': user.email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'Invalid credentials'}), 401
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Login error: {e}")
|
||||||
|
return jsonify({'error': 'Login failed'}), 500
|
||||||
|
|
||||||
|
@app.route('/api/auth/register', methods=['POST'])
|
||||||
|
def register():
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
username = data.get('username')
|
||||||
|
email = data.get('email')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
# Check if user exists
|
||||||
|
if User.query.filter_by(username=username).first():
|
||||||
|
return jsonify({'error': 'Username already exists'}), 400
|
||||||
|
|
||||||
|
if User.query.filter_by(email=email).first():
|
||||||
|
return jsonify({'error': 'Email already exists'}), 400
|
||||||
|
|
||||||
|
# Hash password
|
||||||
|
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||||
|
|
||||||
|
# Create user
|
||||||
|
user = User(username=username, email=email, password_hash=password_hash)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
access_token = create_access_token(identity=user.id)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'access_token': access_token,
|
||||||
|
'user': {
|
||||||
|
'id': user.id,
|
||||||
|
'username': user.username,
|
||||||
|
'email': user.email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Registration error: {e}")
|
||||||
|
return jsonify({'error': 'Registration failed'}), 500
|
||||||
|
|
||||||
|
# Hunt Management Routes
|
||||||
|
@app.route('/api/hunts', methods=['GET'])
|
||||||
|
@jwt_required()
|
||||||
|
def get_hunts():
|
||||||
|
try:
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
hunts = Hunt.query.filter_by(created_by=user_id).all()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'hunts': [{
|
||||||
|
'id': hunt.id,
|
||||||
|
'name': hunt.name,
|
||||||
|
'description': hunt.description,
|
||||||
|
'created_at': hunt.created_at.isoformat(),
|
||||||
|
'status': hunt.status
|
||||||
|
} for hunt in hunts]
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Get hunts error: {e}")
|
||||||
|
return jsonify({'error': 'Failed to fetch hunts'}), 500
|
||||||
|
|
||||||
|
@app.route('/api/hunts', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
def create_hunt():
|
||||||
|
try:
|
||||||
|
user_id = get_jwt_identity()
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
hunt = Hunt(
|
||||||
|
name=data.get('name'),
|
||||||
|
description=data.get('description'),
|
||||||
|
created_by=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(hunt)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'hunt': {
|
||||||
|
'id': hunt.id,
|
||||||
|
'name': hunt.name,
|
||||||
|
'description': hunt.description,
|
||||||
|
'created_at': hunt.created_at.isoformat(),
|
||||||
|
'status': hunt.status
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Create hunt error: {e}")
|
||||||
|
return jsonify({'error': 'Failed to create hunt'}), 500
|
||||||
|
|
||||||
# API Routes
|
# API Routes
|
||||||
@app.route('/api/health')
|
@app.route('/api/health')
|
||||||
def health_check():
|
def health_check():
|
||||||
@@ -135,6 +283,10 @@ def get_stats():
|
|||||||
return jsonify({'error': 'Failed to get stats'}), 500
|
return jsonify({'error': 'Failed to get stats'}), 500
|
||||||
|
|
||||||
# Static file serving for React app
|
# Static file serving for React app
|
||||||
|
@app.route("/assets/<path:path>")
|
||||||
|
def send_assets(path):
|
||||||
|
return send_from_directory(os.path.join(app.static_folder, "assets"), path)
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
if os.path.exists(os.path.join(app.static_folder, "index.html")):
|
if os.path.exists(os.path.join(app.static_folder, "index.html")):
|
||||||
@@ -151,68 +303,6 @@ def index():
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("=" * 50)
|
|
||||||
print("Starting Cyber Threat Hunter Backend...")
|
|
||||||
print("API available at: http://localhost:5000")
|
|
||||||
print("Health check: http://localhost:5000/api/health")
|
|
||||||
print("=" * 50)
|
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
|
||||||
# Basic security tools detection
|
|
||||||
tools_found = []
|
|
||||||
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read().lower()
|
|
||||||
|
|
||||||
# Common security tools to detect
|
|
||||||
security_tools = [
|
|
||||||
'windows defender', 'antimalware', 'avast', 'norton', 'mcafee',
|
|
||||||
'crowdstrike', 'carbon black', 'sentinelone', 'cylance',
|
|
||||||
'kaspersky', 'bitdefender', 'sophos', 'trend micro',
|
|
||||||
'openvpn', 'nordvpn', 'expressvpn', 'cisco anyconnect'
|
|
||||||
]
|
|
||||||
|
|
||||||
for tool in security_tools:
|
|
||||||
if tool in content:
|
|
||||||
tools_found.append(tool)
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'filename': filename,
|
|
||||||
'tools_found': tools_found,
|
|
||||||
'total_tools': len(tools_found)
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Security tools analysis error: {e}")
|
|
||||||
return jsonify({'error': 'Analysis failed'}), 500
|
|
||||||
|
|
||||||
@app.route('/api/stats')
|
|
||||||
def get_stats():
|
|
||||||
try:
|
|
||||||
upload_dir = app.config['UPLOAD_FOLDER']
|
|
||||||
files_count = len([f for f in os.listdir(upload_dir) if os.path.isfile(os.path.join(upload_dir, f))])
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'filesUploaded': files_count,
|
|
||||||
'analysesCompleted': files_count,
|
|
||||||
'threatsDetected': 0
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Stats error: {e}")
|
|
||||||
return jsonify({'error': 'Failed to get stats'}), 500
|
|
||||||
|
|
||||||
# Static file serving for React app
|
|
||||||
@app.route("/assets/<path:path>")
|
|
||||||
def send_assets(path):
|
|
||||||
return send_from_directory(os.path.join(app.static_folder, "assets"), path)
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def index():
|
|
||||||
if os.path.exists(os.path.join(app.static_folder, "index.html")):
|
|
||||||
return send_from_directory(app.static_folder, "index.html")
|
|
||||||
else:
|
|
||||||
return jsonify({'message': 'Cyber Threat Hunter API', 'status': 'running'})
|
|
||||||
|
|
||||||
# Catch-all route for React Router
|
# Catch-all route for React Router
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def catch_all(path):
|
def catch_all(path):
|
||||||
@@ -222,69 +312,12 @@ def catch_all(path):
|
|||||||
return jsonify({'error': 'Frontend not built yet'})
|
return jsonify({'error': 'Frontend not built yet'})
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print("Starting Cyber Threat Hunter Backend...")
|
print("Starting Cyber Threat Hunter Backend...")
|
||||||
print("API available at: http://localhost:5000")
|
print("API available at: http://localhost:5000")
|
||||||
print("Health check: http://localhost:5000/api/health")
|
print("Database: Connected to PostgreSQL")
|
||||||
print("Available endpoints:")
|
|
||||||
print(" POST /api/upload - Upload files")
|
|
||||||
print(" GET /api/files - List uploaded files")
|
|
||||||
print(" GET /api/analyze/<filename> - Analyze file")
|
|
||||||
print(" GET /api/security-tools/<filename> - Detect security tools")
|
|
||||||
print(" GET /api/stats - Get statistics")
|
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
analysis['csv_error'] = str(e)
|
|
||||||
elif filename.lower().endswith('.csv'):
|
|
||||||
# Basic CSV analysis without pandas
|
|
||||||
try:
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
analysis.update({
|
|
||||||
'rows': len(lines) - 1, # Subtract header
|
|
||||||
'columns': len(lines[0].split(',')) if lines else 0,
|
|
||||||
'preview': lines[:6] if lines else []
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
analysis['csv_error'] = str(e)
|
|
||||||
|
|
||||||
return jsonify(analysis)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Analysis error: {e}")
|
|
||||||
return jsonify({'error': 'Analysis failed'}), 500
|
|
||||||
|
|
||||||
# Stats endpoint
|
|
||||||
@app.route('/api/stats')
|
|
||||||
def get_stats():
|
|
||||||
try:
|
|
||||||
upload_dir = app.config['UPLOAD_FOLDER']
|
|
||||||
files_count = len([f for f in os.listdir(upload_dir) if os.path.isfile(os.path.join(upload_dir, f))])
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'filesUploaded': files_count,
|
|
||||||
'analysesCompleted': files_count, # Simplified
|
|
||||||
'threatsDetected': 0 # Placeholder
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Stats error: {e}")
|
|
||||||
return jsonify({'error': 'Failed to get stats'}), 500
|
|
||||||
|
|
||||||
# Static file serving
|
|
||||||
@app.route("/assets/<path:path>")
|
|
||||||
def send_assets(path):
|
|
||||||
return send_from_directory(os.path.join(app.static_folder, "assets"), path)
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def index():
|
|
||||||
return send_from_directory(app.static_folder, "index.html")
|
|
||||||
|
|
||||||
# Catch-all route for React Router
|
|
||||||
@app.route("/<path:path>")
|
|
||||||
def catch_all(path):
|
|
||||||
return send_from_directory(app.static_folder, "index.html")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
flask==3.0.0
|
||||||
|
flask-cors==4.0.0
|
||||||
|
flask-sqlalchemy==3.1.1
|
||||||
|
flask-jwt-extended==4.6.0
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
requests==2.31.0
|
||||||
|
werkzeug==3.0.1
|
||||||
|
bcrypt==4.1.2
|
||||||
|
|||||||
52
database/init.sql
Normal file
52
database/init.sql
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Users table
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
email VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_login TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Hunts table
|
||||||
|
CREATE TABLE hunts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_by UUID REFERENCES users(id),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'completed', 'archived'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Files table
|
||||||
|
CREATE TABLE files (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
hunt_id UUID REFERENCES hunts(id) ON DELETE CASCADE,
|
||||||
|
filename VARCHAR(255) NOT NULL,
|
||||||
|
original_filename VARCHAR(255) NOT NULL,
|
||||||
|
file_size BIGINT,
|
||||||
|
file_type VARCHAR(50),
|
||||||
|
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
analysis_status VARCHAR(20) DEFAULT 'pending' CHECK (analysis_status IN ('pending', 'processing', 'completed', 'failed'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Analysis results table
|
||||||
|
CREATE TABLE analysis_results (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
file_id UUID REFERENCES files(id) ON DELETE CASCADE,
|
||||||
|
analysis_type VARCHAR(50) NOT NULL,
|
||||||
|
results JSONB,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes
|
||||||
|
CREATE INDEX idx_hunts_created_by ON hunts(created_by);
|
||||||
|
CREATE INDEX idx_files_hunt_id ON files(hunt_id);
|
||||||
|
CREATE INDEX idx_analysis_file_id ON analysis_results(file_id);
|
||||||
|
|
||||||
|
-- Insert default admin user (password: admin123)
|
||||||
|
INSERT INTO users (username, email, password_hash) VALUES
|
||||||
|
('admin', 'admin@threathunter.local', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewKyIqhFrMpGLgGi');
|
||||||
31
deploy/aws-deploy.yml
Normal file
31
deploy/aws-deploy.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# AWS ECS Deployment Configuration
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: threat_hunter
|
||||||
|
POSTGRES_USER: ${DB_USER}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: your-registry/threat-hunter-backend:latest
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@database:5432/threat_hunter
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
FLASK_ENV: production
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: your-registry/threat-hunter-frontend:latest
|
||||||
|
ports:
|
||||||
|
- "80:3000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
18
deploy/backup/backup.sh
Normal file
18
deploy/backup/backup.sh
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Database backup
|
||||||
|
BACKUP_DIR="/backups"
|
||||||
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
echo "Creating database backup..."
|
||||||
|
docker exec threat-hunter-db pg_dump -U admin threat_hunter > "$BACKUP_DIR/db_backup_$DATE.sql"
|
||||||
|
|
||||||
|
# File uploads backup
|
||||||
|
echo "Backing up uploads..."
|
||||||
|
tar -czf "$BACKUP_DIR/uploads_backup_$DATE.tar.gz" ./uploads
|
||||||
|
|
||||||
|
# Keep only last 7 days of backups
|
||||||
|
find $BACKUP_DIR -name "*.sql" -mtime +7 -delete
|
||||||
|
find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete
|
||||||
|
|
||||||
|
echo "Backup completed: $DATE"
|
||||||
28
deploy/deploy.sh
Normal file
28
deploy/deploy.sh
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 Deploying Cyber Threat Hunter..."
|
||||||
|
|
||||||
|
# Build and push images
|
||||||
|
echo "📦 Building Docker images..."
|
||||||
|
docker build -t your-registry/threat-hunter-backend:latest ./backend
|
||||||
|
docker build -t your-registry/threat-hunter-frontend:latest ./frontend
|
||||||
|
|
||||||
|
echo "🔄 Pushing to registry..."
|
||||||
|
docker push your-registry/threat-hunter-backend:latest
|
||||||
|
docker push your-registry/threat-hunter-frontend:latest
|
||||||
|
|
||||||
|
# Deploy based on environment
|
||||||
|
if [ "$1" = "kubernetes" ]; then
|
||||||
|
echo "☸️ Deploying to Kubernetes..."
|
||||||
|
kubectl apply -f deploy/kubernetes/
|
||||||
|
elif [ "$1" = "swarm" ]; then
|
||||||
|
echo "🐳 Deploying to Docker Swarm..."
|
||||||
|
docker stack deploy -c deploy/docker-stack.yml threat-hunter
|
||||||
|
else
|
||||||
|
echo "🐙 Deploying with Docker Compose..."
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
55
deploy/docker-stack.yml
Normal file
55
deploy/docker-stack.yml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: threat_hunter
|
||||||
|
POSTGRES_USER_FILE: /run/secrets/db_user
|
||||||
|
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
secrets:
|
||||||
|
- db_user
|
||||||
|
- db_password
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.role == manager
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: your-registry/threat-hunter-backend:latest
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://admin:secure_password_123@database:5432/threat_hunter
|
||||||
|
SECRET_KEY_FILE: /run/secrets/secret_key
|
||||||
|
secrets:
|
||||||
|
- secret_key
|
||||||
|
deploy:
|
||||||
|
replicas: 3
|
||||||
|
update_config:
|
||||||
|
parallelism: 1
|
||||||
|
delay: 10s
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: your-registry/threat-hunter-frontend:latest
|
||||||
|
ports:
|
||||||
|
- "80:3000"
|
||||||
|
deploy:
|
||||||
|
replicas: 2
|
||||||
|
update_config:
|
||||||
|
parallelism: 1
|
||||||
|
delay: 10s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
db_user:
|
||||||
|
external: true
|
||||||
|
db_password:
|
||||||
|
external: true
|
||||||
|
secret_key:
|
||||||
|
external: true
|
||||||
37
deploy/kubernetes/deployment.yaml
Normal file
37
deploy/kubernetes/deployment.yaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: threat-hunter-backend
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: threat-hunter-backend
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: threat-hunter-backend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: backend
|
||||||
|
image: your-registry/threat-hunter-backend:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 5000
|
||||||
|
env:
|
||||||
|
- name: DATABASE_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: threat-hunter-secrets
|
||||||
|
key: database-url
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: threat-hunter-backend-service
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: threat-hunter-backend
|
||||||
|
ports:
|
||||||
|
- port: 5000
|
||||||
|
targetPort: 5000
|
||||||
|
type: LoadBalancer
|
||||||
21
deploy/monitoring/docker-compose.monitoring.yml
Normal file
21
deploy/monitoring/docker-compose.monitoring.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
volumes:
|
||||||
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana
|
||||||
|
ports:
|
||||||
|
- "3001:3000"
|
||||||
|
environment:
|
||||||
|
GF_SECURITY_ADMIN_PASSWORD: admin
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
grafana_data:
|
||||||
26
deploy/security/security-checklist.md
Normal file
26
deploy/security/security-checklist.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Security Deployment Checklist
|
||||||
|
|
||||||
|
## Pre-Deployment
|
||||||
|
- [ ] Change all default passwords
|
||||||
|
- [ ] Generate strong SECRET_KEY
|
||||||
|
- [ ] Setup SSL/TLS certificates
|
||||||
|
- [ ] Configure firewall rules
|
||||||
|
- [ ] Set up backup strategy
|
||||||
|
|
||||||
|
## Database Security
|
||||||
|
- [ ] Use strong database passwords
|
||||||
|
- [ ] Enable database encryption
|
||||||
|
- [ ] Configure database firewall
|
||||||
|
- [ ] Set up regular backups
|
||||||
|
|
||||||
|
## Application Security
|
||||||
|
- [ ] Update all dependencies
|
||||||
|
- [ ] Configure CORS properly
|
||||||
|
- [ ] Enable rate limiting
|
||||||
|
- [ ] Set up monitoring/logging
|
||||||
|
|
||||||
|
## Infrastructure Security
|
||||||
|
- [ ] Use private networks
|
||||||
|
- [ ] Configure load balancer
|
||||||
|
- [ ] Set up intrusion detection
|
||||||
|
- [ ] Regular security updates
|
||||||
29
deploy/setup-prod.bat
Normal file
29
deploy/setup-prod.bat
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
@echo off
|
||||||
|
echo Setting up production environment...
|
||||||
|
|
||||||
|
REM Create environment file
|
||||||
|
echo Creating .env.prod file...
|
||||||
|
(
|
||||||
|
echo DB_USER=threat_hunter_user
|
||||||
|
echo DB_PASSWORD=%RANDOM%%RANDOM%
|
||||||
|
echo SECRET_KEY=%RANDOM%%RANDOM%%RANDOM%
|
||||||
|
echo FLASK_ENV=production
|
||||||
|
) > .env.prod
|
||||||
|
|
||||||
|
REM Setup SSL certificates
|
||||||
|
echo Setting up SSL certificates...
|
||||||
|
mkdir ssl
|
||||||
|
REM Add your SSL certificate generation here
|
||||||
|
|
||||||
|
REM Create backup directory
|
||||||
|
mkdir backups
|
||||||
|
mkdir logs
|
||||||
|
|
||||||
|
REM Setup firewall rules
|
||||||
|
echo Configuring firewall...
|
||||||
|
netsh advfirewall firewall add rule name="HTTP" dir=in action=allow protocol=TCP localport=80
|
||||||
|
netsh advfirewall firewall add rule name="HTTPS" dir=in action=allow protocol=TCP localport=443
|
||||||
|
|
||||||
|
echo Production setup complete!
|
||||||
|
echo Please update .env.prod with your actual values
|
||||||
|
pause
|
||||||
72
docker-compose.prod.yml
Normal file
72
docker-compose.prod.yml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: threat_hunter
|
||||||
|
POSTGRES_USER: ${DB_USER}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./backups:/backups
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile.prod
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@database:5432/threat_hunter
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
FLASK_ENV: production
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
- ./logs:/app/logs
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
- web
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile.prod
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./ssl:/etc/ssl/certs
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
- ./ssl:/etc/ssl/certs
|
||||||
|
depends_on:
|
||||||
|
- frontend
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
|
internal:
|
||||||
|
internal: true
|
||||||
55
docker-compose.yml
Normal file
55
docker-compose.yml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: threat-hunter-db
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: threat_hunter
|
||||||
|
POSTGRES_USER: admin
|
||||||
|
POSTGRES_PASSWORD: secure_password_123
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
networks:
|
||||||
|
- threat-hunter-network
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: threat-hunter-backend
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://admin:secure_password_123@database:5432/threat_hunter
|
||||||
|
FLASK_ENV: production
|
||||||
|
SECRET_KEY: your-secret-key-change-in-production
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
- ./output:/app/output
|
||||||
|
networks:
|
||||||
|
- threat-hunter-network
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: threat-hunter-frontend
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- threat-hunter-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
threat-hunter-network:
|
||||||
|
driver: bridge
|
||||||
20
docker-start.bat
Normal file
20
docker-start.bat
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
@echo off
|
||||||
|
echo Starting Cyber Threat Hunter with Docker...
|
||||||
|
|
||||||
|
docker-compose down
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Cyber Threat Hunter is starting...
|
||||||
|
echo Frontend: http://localhost:3000
|
||||||
|
echo Backend API: http://localhost:5000
|
||||||
|
echo Database: PostgreSQL on port 5432
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
echo Default credentials:
|
||||||
|
echo Username: admin
|
||||||
|
echo Password: admin123
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
20
frontend/Dockerfile
Normal file
20
frontend/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Serve with a simple server
|
||||||
|
RUN npm install -g serve
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["serve", "-s", "dist", "-l", "3000"]
|
||||||
19
frontend/Dockerfile.prod
Normal file
19
frontend/Dockerfile.prod
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:18-alpine as build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
EXPOSE 80 443
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Suspense } from "react";
|
import React, { Suspense, useState, useEffect } from "react";
|
||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
|
||||||
import { CssBaseline } from "@mui/material";
|
import { CssBaseline } from "@mui/material";
|
||||||
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
||||||
|
|
||||||
@@ -12,6 +12,9 @@ import CSVProcessing from "./pages/CSVProcessing";
|
|||||||
import SettingsConfig from "./pages/SettingsConfig";
|
import SettingsConfig from "./pages/SettingsConfig";
|
||||||
import VirusTotal from "./pages/VirusTotal";
|
import VirusTotal from "./pages/VirusTotal";
|
||||||
import SecurityTools from "./pages/SecurityTools";
|
import SecurityTools from "./pages/SecurityTools";
|
||||||
|
import LoginPage from "./pages/LoginPage";
|
||||||
|
import Dashboard from "./pages/Dashboard";
|
||||||
|
import HuntPage from "./pages/HuntPage";
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@@ -20,6 +23,27 @@ const theme = createTheme({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (token) {
|
||||||
|
// Verify token and get user info
|
||||||
|
// For now, just set a dummy user
|
||||||
|
setUser({ username: "User" });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
|
||||||
|
<div className="text-white">Loading...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
@@ -29,7 +53,10 @@ function App() {
|
|||||||
<main className="flex-1 p-6 overflow-auto">
|
<main className="flex-1 p-6 overflow-auto">
|
||||||
<Suspense fallback={<div className="text-white">Loading...</div>}>
|
<Suspense fallback={<div className="text-white">Loading...</div>}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/login" element={!user ? <LoginPage onLogin={setUser} /> : <Navigate to="/dashboard" />} />
|
||||||
|
<Route path="/dashboard" element={user ? <Dashboard user={user} /> : <Navigate to="/login" />} />
|
||||||
|
<Route path="/hunt/:huntId" element={user ? <HuntPage user={user} /> : <Navigate to="/login" />} />
|
||||||
|
<Route path="/" element={<Navigate to={user ? "/dashboard" : "/login"} />} />
|
||||||
<Route path="/baseline" element={<Baseline />} />
|
<Route path="/baseline" element={<Baseline />} />
|
||||||
<Route path="/networking" element={<Networking />} />
|
<Route path="/networking" element={<Networking />} />
|
||||||
<Route path="/applications" element={<Applications />} />
|
<Route path="/applications" element={<Applications />} />
|
||||||
|
|||||||
149
frontend/src/pages/Dashboard.jsx
Normal file
149
frontend/src/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { Plus, FolderOpen, Calendar, User } from 'lucide-react'
|
||||||
|
|
||||||
|
const Dashboard = ({ user }) => {
|
||||||
|
const [hunts, setHunts] = useState([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [showNewHunt, setShowNewHunt] = useState(false)
|
||||||
|
const [newHunt, setNewHunt] = useState({ name: '', description: '' })
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchHunts()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchHunts = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await fetch('/api/hunts', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
setHunts(data.hunts)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch hunts:', err)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createHunt = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await fetch('/api/hunts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newHunt)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
navigate(`/hunt/${data.hunt.id}`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create hunt:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="flex items-center justify-center h-64">Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Welcome back, {user.username}</h1>
|
||||||
|
<p className="text-zinc-400">Choose a hunt to continue or start a new investigation</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewHunt(true)}
|
||||||
|
className="bg-cyan-600 hover:bg-cyan-700 text-white px-4 py-2 rounded-lg flex items-center"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
New Hunt
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showNewHunt && (
|
||||||
|
<div className="bg-zinc-800 p-6 rounded-lg">
|
||||||
|
<h2 className="text-xl font-bold mb-4">Create New Hunt</h2>
|
||||||
|
<form onSubmit={createHunt} className="space-y-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Hunt Name"
|
||||||
|
value={newHunt.name}
|
||||||
|
onChange={(e) => setNewHunt({...newHunt, name: e.target.value})}
|
||||||
|
className="w-full p-3 bg-zinc-700 rounded-lg border border-zinc-600 focus:border-cyan-400 outline-none"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
placeholder="Description"
|
||||||
|
value={newHunt.description}
|
||||||
|
onChange={(e) => setNewHunt({...newHunt, description: e.target.value})}
|
||||||
|
className="w-full p-3 bg-zinc-700 rounded-lg border border-zinc-600 focus:border-cyan-400 outline-none h-24"
|
||||||
|
/>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button type="submit" className="bg-cyan-600 hover:bg-cyan-700 text-white px-4 py-2 rounded-lg">
|
||||||
|
Create Hunt
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowNewHunt(false)}
|
||||||
|
className="bg-zinc-600 hover:bg-zinc-700 text-white px-4 py-2 rounded-lg"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{hunts.map(hunt => (
|
||||||
|
<div
|
||||||
|
key={hunt.id}
|
||||||
|
onClick={() => navigate(`/hunt/${hunt.id}`)}
|
||||||
|
className="bg-zinc-800 p-6 rounded-lg cursor-pointer hover:bg-zinc-750 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<FolderOpen className="w-8 h-8 text-cyan-400 mr-3" />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">{hunt.name}</h3>
|
||||||
|
<p className="text-sm text-zinc-400">{hunt.status}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-zinc-400 text-sm mb-3">{hunt.description}</p>
|
||||||
|
<div className="flex items-center text-xs text-zinc-500">
|
||||||
|
<Calendar className="w-3 h-3 mr-1" />
|
||||||
|
{new Date(hunt.created_at).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hunts.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<FolderOpen className="w-16 h-16 text-zinc-600 mx-auto mb-4" />
|
||||||
|
<h3 className="text-xl font-semibold mb-2">No hunts yet</h3>
|
||||||
|
<p className="text-zinc-400 mb-4">Start your first threat hunting investigation</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewHunt(true)}
|
||||||
|
className="bg-cyan-600 hover:bg-cyan-700 text-white px-6 py-3 rounded-lg"
|
||||||
|
>
|
||||||
|
Create Your First Hunt
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard
|
||||||
104
frontend/src/pages/LoginPage.jsx
Normal file
104
frontend/src/pages/LoginPage.jsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { Shield, User, Lock } from 'lucide-react'
|
||||||
|
|
||||||
|
const LoginPage = ({ onLogin }) => {
|
||||||
|
const [credentials, setCredentials] = useState({ username: '', password: '' })
|
||||||
|
const [isLogin, setIsLogin] = useState(true)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpoint = isLogin ? '/api/auth/login' : '/api/auth/register'
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(credentials)
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
localStorage.setItem('token', data.access_token)
|
||||||
|
onLogin(data.user)
|
||||||
|
navigate('/dashboard')
|
||||||
|
} else {
|
||||||
|
setError(data.error)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Connection failed')
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-zinc-900 flex items-center justify-center">
|
||||||
|
<div className="bg-zinc-800 p-8 rounded-lg shadow-lg w-96">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<Shield className="w-16 h-16 text-cyan-400 mx-auto mb-4" />
|
||||||
|
<h1 className="text-2xl font-bold">Cyber Threat Hunter</h1>
|
||||||
|
<p className="text-zinc-400">Advanced threat hunting platform</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Username</label>
|
||||||
|
<div className="relative">
|
||||||
|
<User className="w-5 h-5 absolute left-3 top-3 text-zinc-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={credentials.username}
|
||||||
|
onChange={(e) => setCredentials({...credentials, username: e.target.value})}
|
||||||
|
className="w-full pl-10 p-3 bg-zinc-700 rounded-lg border border-zinc-600 focus:border-cyan-400 outline-none"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">Password</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="w-5 h-5 absolute left-3 top-3 text-zinc-400" />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={credentials.password}
|
||||||
|
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
||||||
|
className="w-full pl-10 p-3 bg-zinc-700 rounded-lg border border-zinc-600 focus:border-cyan-400 outline-none"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-red-400 text-sm">{error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-cyan-600 hover:bg-cyan-700 text-white p-3 rounded-lg disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Please wait...' : (isLogin ? 'Login' : 'Register')}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="text-center mt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsLogin(!isLogin)}
|
||||||
|
className="text-cyan-400 hover:text-cyan-300"
|
||||||
|
>
|
||||||
|
{isLogin ? 'Need an account? Register' : 'Already have an account? Login'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage
|
||||||
Reference in New Issue
Block a user