mirror of
https://github.com/mblanke/ThreatHunt.git
synced 2026-03-01 05:50:21 -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 logging
|
||||
from datetime import datetime, timedelta
|
||||
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.exceptions import RequestEntityTooLarge
|
||||
from datetime import datetime
|
||||
import bcrypt
|
||||
|
||||
# Try to import flask-cors
|
||||
try:
|
||||
@@ -19,6 +21,24 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
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
|
||||
if CORS_AVAILABLE:
|
||||
CORS(app)
|
||||
@@ -30,16 +50,6 @@ else:
|
||||
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
|
||||
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):
|
||||
return '.' in filename and \
|
||||
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
|
||||
@@ -53,6 +63,144 @@ def handle_exception(e):
|
||||
logger.error(f"Unhandled exception: {e}")
|
||||
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
|
||||
@app.route('/api/health')
|
||||
def health_check():
|
||||
@@ -135,6 +283,10 @@ def get_stats():
|
||||
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")):
|
||||
@@ -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
|
||||
@app.route("/<path:path>")
|
||||
def catch_all(path):
|
||||
@@ -222,69 +312,12 @@ def catch_all(path):
|
||||
return jsonify({'error': 'Frontend not built yet'})
|
||||
|
||||
if __name__ == "__main__":
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
print("=" * 50)
|
||||
print("Starting Cyber Threat Hunter Backend...")
|
||||
print("API available at: http://localhost:5000")
|
||||
print("Health check: http://localhost:5000/api/health")
|
||||
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("Database: Connected to PostgreSQL")
|
||||
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)
|
||||
|
||||
@@ -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 { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import React, { Suspense, useState, useEffect } from "react";
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
||||
|
||||
@@ -12,6 +12,9 @@ import CSVProcessing from "./pages/CSVProcessing";
|
||||
import SettingsConfig from "./pages/SettingsConfig";
|
||||
import VirusTotal from "./pages/VirusTotal";
|
||||
import SecurityTools from "./pages/SecurityTools";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import HuntPage from "./pages/HuntPage";
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
@@ -20,6 +23,27 @@ const theme = createTheme({
|
||||
});
|
||||
|
||||
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 (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
@@ -29,7 +53,10 @@ function App() {
|
||||
<main className="flex-1 p-6 overflow-auto">
|
||||
<Suspense fallback={<div className="text-white">Loading...</div>}>
|
||||
<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="/networking" element={<Networking />} />
|
||||
<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