mirror of
https://github.com/mblanke/StrikePackageGPT.git
synced 2026-03-01 14:20:21 -05:00
Compare commits
7 Commits
V2.1
...
copilot/ex
| Author | SHA1 | Date | |
|---|---|---|---|
| bfb52f098c | |||
| e459266e9c | |||
| af31caeacf | |||
| b971482bbd | |||
| 17f8a332db | |||
| b1250aa452 | |||
| 8b51ba9108 |
26
.devcontainer/docker-compose.yml
Normal file
26
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
# Update this to the name of the service you want to work with in your docker-compose.yml file
|
||||
dashboard:
|
||||
# Uncomment if you want to override the service's Dockerfile to one in the .devcontainer
|
||||
# folder. Note that the path of the Dockerfile and context is relative to the *primary*
|
||||
# docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
|
||||
# array). The sample below assumes your primary file is in the root of your project.
|
||||
#
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: .devcontainer/Dockerfile
|
||||
|
||||
volumes:
|
||||
# Update this to wherever you want VS Code to mount the folder of your project
|
||||
- ..:/workspaces:cached
|
||||
|
||||
# Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
|
||||
# cap_add:
|
||||
# - SYS_PTRACE
|
||||
# security_opt:
|
||||
# - seccomp:unconfined
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for more information:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# https://containers.dev/guide/dependabot
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -32,3 +32,6 @@ data/
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Nested repo
|
||||
StrikePackageGPT/
|
||||
|
||||
@@ -11,6 +11,8 @@ services:
|
||||
- HACKGPT_API_URL=http://strikepackage-hackgpt-api:8001
|
||||
- LLM_ROUTER_URL=http://strikepackage-llm-router:8000
|
||||
- KALI_EXECUTOR_URL=http://strikepackage-kali-executor:8002
|
||||
volumes:
|
||||
- ./data/dashboard:/app/data
|
||||
depends_on:
|
||||
- hackgpt-api
|
||||
- llm-router
|
||||
@@ -67,15 +69,22 @@ services:
|
||||
environment:
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||
# Multi-endpoint support: comma-separated URLs
|
||||
- OLLAMA_ENDPOINTS=${OLLAMA_ENDPOINTS:-http://192.168.1.50:11434}
|
||||
# Prefer local Ollama container for self-contained setup
|
||||
- OLLAMA_LOCAL_URL=${OLLAMA_LOCAL_URL:-http://strikepackage-ollama:11434}
|
||||
# Network Ollama instances (Dell LLM box with larger models)
|
||||
- OLLAMA_NETWORK_URLS=${OLLAMA_NETWORK_URLS:-http://192.168.1.50:11434}
|
||||
# Legacy single endpoint (fallback)
|
||||
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://192.168.1.50:11434}
|
||||
- OLLAMA_ENDPOINTS=${OLLAMA_ENDPOINTS:-http://strikepackage-ollama:11434}
|
||||
- OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://strikepackage-ollama:11434}
|
||||
# Load balancing: round-robin, random, failover
|
||||
- LOAD_BALANCE_STRATEGY=${LOAD_BALANCE_STRATEGY:-round-robin}
|
||||
- LOAD_BALANCE_STRATEGY=${LOAD_BALANCE_STRATEGY:-failover}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- strikepackage-net
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- ollama
|
||||
|
||||
# Kali Linux - Security tools container
|
||||
kali:
|
||||
@@ -95,26 +104,25 @@ services:
|
||||
- NET_RAW
|
||||
restart: unless-stopped
|
||||
|
||||
# Ollama - Local LLM (disabled - using Dell LLM box at 192.168.1.50)
|
||||
# Uncomment to use local Ollama instead
|
||||
# ollama:
|
||||
# image: ollama/ollama:latest
|
||||
# container_name: strikepackage-ollama
|
||||
# ports:
|
||||
# - "11434:11434"
|
||||
# volumes:
|
||||
# - ollama-models:/root/.ollama
|
||||
# networks:
|
||||
# - strikepackage-net
|
||||
# restart: unless-stopped
|
||||
# # Uncomment for GPU support:
|
||||
# # deploy:
|
||||
# # resources:
|
||||
# # reservations:
|
||||
# # devices:
|
||||
# # - driver: nvidia
|
||||
# # count: all
|
||||
# # capabilities: [gpu]
|
||||
# Ollama - Local LLM
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
container_name: strikepackage-ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
networks:
|
||||
- strikepackage-net
|
||||
restart: unless-stopped
|
||||
# GPU support (optional): uncomment if using NVIDIA GPU
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: all
|
||||
# capabilities: [gpu]
|
||||
|
||||
networks:
|
||||
strikepackage-net:
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
FROM node:20-slim AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy package files and JSX components
|
||||
COPY package.json vite.config.js ./
|
||||
COPY components/ ./components/
|
||||
|
||||
# Install dependencies and build
|
||||
RUN npm install && npm run build
|
||||
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
@@ -11,6 +22,9 @@ COPY app/ ./app/
|
||||
COPY templates/ ./templates/
|
||||
COPY static/ ./static/
|
||||
|
||||
# Copy built components from builder stage
|
||||
COPY --from=builder /build/static/dist/ ./static/dist/
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
156
services/dashboard/components/GuidedWizard.jsx
Normal file
156
services/dashboard/components/GuidedWizard.jsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const WIZARD_TYPES = {
|
||||
first_time_setup: {
|
||||
title: 'Welcome to GooseStrike',
|
||||
steps: [
|
||||
{ id: 'intro', title: 'Introduction', icon: '👋' },
|
||||
{ id: 'phases', title: 'Methodology', icon: '📋' },
|
||||
{ id: 'tools', title: 'Security Tools', icon: '🛠️' },
|
||||
{ id: 'start', title: 'Get Started', icon: '🚀' },
|
||||
],
|
||||
},
|
||||
run_scan: {
|
||||
title: 'Run Security Scan',
|
||||
steps: [
|
||||
{ id: 'target', title: 'Target Selection', icon: '🎯' },
|
||||
{ id: 'scan-type', title: 'Scan Type', icon: '🔍' },
|
||||
{ id: 'options', title: 'Options', icon: '⚙️' },
|
||||
{ id: 'execute', title: 'Execute', icon: '▶️' },
|
||||
],
|
||||
},
|
||||
create_operation: {
|
||||
title: 'Create Security Operation',
|
||||
steps: [
|
||||
{ id: 'details', title: 'Operation Details', icon: '📝' },
|
||||
{ id: 'scope', title: 'Target Scope', icon: '🎯' },
|
||||
{ id: 'methodology', title: 'Methodology', icon: '📋' },
|
||||
{ id: 'review', title: 'Review', icon: '✅' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const GuidedWizard = ({ type = 'first_time_setup', onComplete = () => {}, onCancel = () => {} }) => {
|
||||
const wizard = WIZARD_TYPES[type] || WIZARD_TYPES.first_time_setup;
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [formData, setFormData] = useState({});
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < wizard.steps.length - 1) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
} else {
|
||||
onComplete(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (key, value) => {
|
||||
setFormData({ ...formData, [key]: value });
|
||||
};
|
||||
|
||||
const progress = ((currentStep + 1) / wizard.steps.length) * 100;
|
||||
|
||||
return (
|
||||
<div className="guided-wizard fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div className="bg-sp-dark rounded-lg border border-sp-grey-mid w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-sp-grey-mid">
|
||||
<h2 className="text-2xl font-bold text-sp-white">{wizard.title}</h2>
|
||||
<div className="mt-4 flex gap-2">
|
||||
{wizard.steps.map((step, idx) => (
|
||||
<div
|
||||
key={step.id}
|
||||
className={`wizard-step flex-1 p-2 rounded text-center border transition ${
|
||||
idx === currentStep
|
||||
? 'border-sp-red bg-sp-red bg-opacity-10'
|
||||
: idx < currentStep
|
||||
? 'border-green-500 bg-green-500 bg-opacity-10'
|
||||
: 'border-sp-grey-mid'
|
||||
}`}
|
||||
>
|
||||
<div className="text-xl">{step.icon}</div>
|
||||
<div className="text-xs text-sp-white-muted mt-1">{step.title}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 h-1 bg-sp-grey-mid rounded overflow-hidden">
|
||||
<div
|
||||
className="wizard-progress h-full bg-sp-red transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 p-6 overflow-y-auto">
|
||||
<div className="text-sp-white">
|
||||
{/* Render step content based on wizard type and current step */}
|
||||
{type === 'first_time_setup' && currentStep === 0 && (
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-4">Welcome to GooseStrike! 🍁</h3>
|
||||
<p className="text-sp-white-muted mb-4">
|
||||
GooseStrike is an AI-powered penetration testing platform that follows industry-standard
|
||||
methodologies to help you identify security vulnerabilities.
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-sp-white-muted space-y-2">
|
||||
<li>AI-assisted security analysis with local or cloud LLMs</li>
|
||||
<li>600+ integrated Kali Linux security tools</li>
|
||||
<li>Voice control for hands-free operation</li>
|
||||
<li>Interactive network visualization</li>
|
||||
<li>Comprehensive reporting and documentation</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{type === 'run_scan' && currentStep === 0 && (
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-4">Select Target</h3>
|
||||
<label className="block mb-2 text-sm text-sp-white-muted">Target IP or hostname</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-sp-grey border border-sp-grey-mid rounded px-3 py-2 text-sp-white"
|
||||
placeholder="192.168.1.100 or example.com"
|
||||
value={formData.target || ''}
|
||||
onChange={(e) => handleInputChange('target', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Add more step content as needed */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-6 border-t border-sp-grey-mid flex justify-between">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 bg-sp-grey hover:bg-sp-grey-light rounded text-sp-white transition"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
{currentStep > 0 && (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="px-4 py-2 bg-sp-grey hover:bg-sp-grey-light rounded text-sp-white transition"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="px-4 py-2 bg-sp-red hover:bg-sp-red-dark rounded text-sp-white transition"
|
||||
>
|
||||
{currentStep === wizard.steps.length - 1 ? 'Complete' : 'Next →'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuidedWizard;
|
||||
110
services/dashboard/components/NetworkMap.jsx
Normal file
110
services/dashboard/components/NetworkMap.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import cytoscape from 'cytoscape';
|
||||
|
||||
const NetworkMap = ({ hosts = [], onHostSelect = () => {} }) => {
|
||||
const containerRef = useRef(null);
|
||||
const cyRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || hosts.length === 0) return;
|
||||
|
||||
// Build Cytoscape elements from hosts
|
||||
const elements = [];
|
||||
|
||||
hosts.forEach((host) => {
|
||||
elements.push({
|
||||
data: {
|
||||
id: host.ip,
|
||||
label: host.hostname || host.ip,
|
||||
type: host.device_type || 'unknown',
|
||||
os: host.os || 'unknown',
|
||||
ports: host.ports || [],
|
||||
},
|
||||
classes: host.device_type || 'unknown',
|
||||
});
|
||||
|
||||
// Add edges for network relationships (simple example: connect all to a central gateway)
|
||||
if (host.ip !== '192.168.1.1') {
|
||||
elements.push({
|
||||
data: {
|
||||
id: `edge-${host.ip}`,
|
||||
source: '192.168.1.1',
|
||||
target: host.ip,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Cytoscape
|
||||
cyRef.current = cytoscape({
|
||||
container: containerRef.current,
|
||||
elements,
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': '#dc2626',
|
||||
label: 'data(label)',
|
||||
color: '#e5e5e5',
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'font-size': '10px',
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'node.router',
|
||||
style: {
|
||||
'background-color': '#3b82f6',
|
||||
shape: 'diamond',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'node.server',
|
||||
style: {
|
||||
'background-color': '#22c55e',
|
||||
shape: 'rectangle',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
width: 2,
|
||||
'line-color': '#3a3a3a',
|
||||
'target-arrow-color': '#3a3a3a',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'curve-style': 'bezier',
|
||||
},
|
||||
},
|
||||
],
|
||||
layout: {
|
||||
name: 'cose',
|
||||
animate: true,
|
||||
animationDuration: 500,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle node clicks
|
||||
cyRef.current.on('tap', 'node', (evt) => {
|
||||
const node = evt.target;
|
||||
onHostSelect(node.data());
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (cyRef.current) {
|
||||
cyRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, [hosts, onHostSelect]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="network-map-container w-full h-full min-h-[500px] rounded border border-sp-grey-mid"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkMap;
|
||||
124
services/dashboard/components/VoiceControls.jsx
Normal file
124
services/dashboard/components/VoiceControls.jsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
|
||||
const VoiceControls = ({ onCommand = () => {}, apiUrl = '/api/voice' }) => {
|
||||
const [state, setState] = useState('idle'); // idle, listening, processing
|
||||
const [transcript, setTranscript] = useState('');
|
||||
const [hotkey, setHotkey] = useState('`'); // backtick
|
||||
const mediaRecorderRef = useRef(null);
|
||||
const audioChunksRef = useRef([]);
|
||||
const hotkeyPressedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === hotkey && !hotkeyPressedRef.current && state === 'idle') {
|
||||
hotkeyPressedRef.current = true;
|
||||
startRecording();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (e) => {
|
||||
if (e.key === hotkey && hotkeyPressedRef.current) {
|
||||
hotkeyPressedRef.current = false;
|
||||
stopRecording();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
};
|
||||
}, [state, hotkey]);
|
||||
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorderRef.current = new MediaRecorder(stream);
|
||||
audioChunksRef.current = [];
|
||||
|
||||
mediaRecorderRef.current.ondataavailable = (event) => {
|
||||
audioChunksRef.current.push(event.data);
|
||||
};
|
||||
|
||||
mediaRecorderRef.current.onstop = async () => {
|
||||
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/wav' });
|
||||
await sendToTranscribe(audioBlob);
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
};
|
||||
|
||||
mediaRecorderRef.current.start();
|
||||
setState('listening');
|
||||
setTranscript('Listening...');
|
||||
} catch (error) {
|
||||
console.error('Microphone access denied:', error);
|
||||
setTranscript('Microphone access denied');
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
if (mediaRecorderRef.current && state === 'listening') {
|
||||
mediaRecorderRef.current.stop();
|
||||
setState('processing');
|
||||
setTranscript('Processing...');
|
||||
}
|
||||
};
|
||||
|
||||
const sendToTranscribe = async (audioBlob) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('audio', audioBlob, 'recording.wav');
|
||||
|
||||
const response = await fetch(`${apiUrl}/transcribe`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
setTranscript(result.text || 'No speech detected');
|
||||
setState('idle');
|
||||
|
||||
if (result.text) {
|
||||
onCommand(result.text);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Transcription failed:', error);
|
||||
setTranscript('Transcription failed');
|
||||
setState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="voice-controls p-4 bg-sp-grey rounded border border-sp-grey-mid">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className={`voice-btn w-12 h-12 rounded-full flex items-center justify-center text-2xl transition ${
|
||||
state === 'listening'
|
||||
? 'bg-sp-red animate-pulse'
|
||||
: state === 'processing'
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-sp-grey-mid hover:bg-sp-red'
|
||||
}`}
|
||||
onMouseDown={startRecording}
|
||||
onMouseUp={stopRecording}
|
||||
disabled={state === 'processing'}
|
||||
>
|
||||
{state === 'listening' ? '🎙️' : state === 'processing' ? '⏳' : '🎤'}
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-sp-white-muted">
|
||||
{state === 'idle' && `Press & hold ${hotkey} or click to speak`}
|
||||
{state === 'listening' && 'Release to stop recording'}
|
||||
{state === 'processing' && 'Processing audio...'}
|
||||
</div>
|
||||
{transcript && (
|
||||
<div className="text-sm text-sp-white mt-1 font-mono">{transcript}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VoiceControls;
|
||||
40
services/dashboard/components/index.jsx
Normal file
40
services/dashboard/components/index.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import VoiceControls from './VoiceControls';
|
||||
import NetworkMap from './NetworkMap';
|
||||
import GuidedWizard from './GuidedWizard';
|
||||
|
||||
// Export components for external mounting
|
||||
window.GooseStrikeComponents = {
|
||||
VoiceControls,
|
||||
NetworkMap,
|
||||
GuidedWizard,
|
||||
mount: {
|
||||
voiceControls: (containerId, props = {}) => {
|
||||
const container = document.getElementById(containerId);
|
||||
if (container) {
|
||||
const root = createRoot(container);
|
||||
root.render(<VoiceControls {...props} />);
|
||||
return root;
|
||||
}
|
||||
},
|
||||
networkMap: (containerId, props = {}) => {
|
||||
const container = document.getElementById(containerId);
|
||||
if (container) {
|
||||
const root = createRoot(container);
|
||||
root.render(<NetworkMap {...props} />);
|
||||
return root;
|
||||
}
|
||||
},
|
||||
guidedWizard: (containerId, props = {}) => {
|
||||
const container = document.getElementById(containerId);
|
||||
if (container) {
|
||||
const root = createRoot(container);
|
||||
root.render(<GuidedWizard {...props} />);
|
||||
return root;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export { VoiceControls, NetworkMap, GuidedWizard };
|
||||
19
services/dashboard/package.json
Normal file
19
services/dashboard/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "goosestrike-dashboard",
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"cytoscape": "^3.28.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
||||
@@ -3,3 +3,4 @@ uvicorn[standard]==0.32.1
|
||||
httpx==0.28.1
|
||||
pydantic==2.10.2
|
||||
jinja2==3.1.4
|
||||
websockets==12.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
21
services/dashboard/vite.config.js
Normal file
21
services/dashboard/vite.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'static/dist',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
components: path.resolve(__dirname, 'components/index.jsx'),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: 'components.js',
|
||||
chunkFileNames: 'components-[name].js',
|
||||
assetFileNames: 'components-[name].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -4,6 +4,7 @@ Executes commands in the Kali container via Docker SDK.
|
||||
"""
|
||||
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, Any, List
|
||||
import docker
|
||||
@@ -13,6 +14,8 @@ import os
|
||||
import uuid
|
||||
import json
|
||||
import re
|
||||
import httpx
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@@ -101,6 +104,307 @@ def validate_command(command: str) -> tuple[bool, str]:
|
||||
return True, "OK"
|
||||
|
||||
|
||||
# Dashboard URL for sending discovered hosts
|
||||
DASHBOARD_URL = os.getenv("DASHBOARD_URL", "http://dashboard:8080")
|
||||
|
||||
|
||||
def is_nmap_command(command: str) -> bool:
|
||||
"""Check if command is an nmap scan that might discover hosts."""
|
||||
parts = command.strip().split()
|
||||
if not parts:
|
||||
return False
|
||||
base_cmd = parts[0].split("/")[-1]
|
||||
return base_cmd == "nmap" or base_cmd == "masscan"
|
||||
|
||||
|
||||
def detect_os_type(os_string: str) -> str:
|
||||
"""Detect OS type from nmap OS string."""
|
||||
if not os_string:
|
||||
return ""
|
||||
os_lower = os_string.lower()
|
||||
|
||||
if "windows" in os_lower:
|
||||
return "Windows"
|
||||
elif any(x in os_lower for x in ["linux", "ubuntu", "debian", "centos", "red hat"]):
|
||||
return "Linux"
|
||||
elif any(x in os_lower for x in ["mac os", "darwin", "apple", "ios"]):
|
||||
return "macOS"
|
||||
elif "cisco" in os_lower:
|
||||
return "Cisco Router"
|
||||
elif "juniper" in os_lower:
|
||||
return "Juniper Router"
|
||||
elif any(x in os_lower for x in ["fortinet", "fortigate"]):
|
||||
return "Fortinet"
|
||||
elif any(x in os_lower for x in ["vmware", "esxi"]):
|
||||
return "VMware Server"
|
||||
elif "freebsd" in os_lower:
|
||||
return "FreeBSD"
|
||||
elif "android" in os_lower:
|
||||
return "Android"
|
||||
elif any(x in os_lower for x in ["printer", "hp"]):
|
||||
return "Printer"
|
||||
elif "switch" in os_lower:
|
||||
return "Network Switch"
|
||||
elif "router" in os_lower:
|
||||
return "Router"
|
||||
return ""
|
||||
|
||||
|
||||
def infer_os_from_ports(ports: List[Dict]) -> str:
|
||||
"""Infer OS type from open ports.
|
||||
|
||||
Uses a scoring system to handle hosts running multiple services
|
||||
(e.g., Linux with Samba looks like Windows on port 445).
|
||||
"""
|
||||
port_nums = {p["port"] for p in ports}
|
||||
services = {p.get("service", "").lower() for p in ports}
|
||||
products = [p.get("product", "").lower() for p in ports]
|
||||
|
||||
# Score-based detection to handle mixed indicators
|
||||
linux_score = 0
|
||||
windows_score = 0
|
||||
|
||||
# Strong Linux indicators
|
||||
if 22 in port_nums: # SSH is strongly Linux/Unix
|
||||
linux_score += 3
|
||||
if any("openssh" in p or "linux" in p for p in products):
|
||||
linux_score += 5
|
||||
if any("apache" in p or "nginx" in p for p in products):
|
||||
linux_score += 2
|
||||
|
||||
# Strong Windows indicators
|
||||
if 135 in port_nums: # MSRPC is Windows-only
|
||||
windows_score += 5
|
||||
if 3389 in port_nums: # RDP is Windows
|
||||
windows_score += 3
|
||||
if 5985 in port_nums or 5986 in port_nums: # WinRM is Windows-only
|
||||
windows_score += 5
|
||||
if any("microsoft" in p or "windows" in p for p in products):
|
||||
windows_score += 5
|
||||
|
||||
# Weak indicators (could be either)
|
||||
if 445 in port_nums: # SMB - could be Samba on Linux or Windows
|
||||
windows_score += 1 # Slight Windows bias but not definitive
|
||||
if 139 in port_nums: # NetBIOS - same as above
|
||||
windows_score += 1
|
||||
|
||||
# Decide based on score
|
||||
if linux_score > windows_score:
|
||||
return "Linux"
|
||||
if windows_score > linux_score:
|
||||
return "Windows"
|
||||
|
||||
# Network device indicators
|
||||
if 161 in port_nums or 162 in port_nums:
|
||||
return "Network Device"
|
||||
|
||||
# Printer
|
||||
if 9100 in port_nums or 631 in port_nums:
|
||||
return "Printer"
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def parse_nmap_output(stdout: str) -> List[Dict[str, Any]]:
|
||||
"""Parse nmap output (XML or text) and extract discovered hosts."""
|
||||
hosts = []
|
||||
|
||||
# Try XML parsing first (if -oX - was used or combined with other options)
|
||||
if '<?xml' in stdout or '<nmaprun' in stdout:
|
||||
try:
|
||||
xml_start = stdout.find('<?xml')
|
||||
if xml_start == -1:
|
||||
xml_start = stdout.find('<nmaprun')
|
||||
if xml_start != -1:
|
||||
xml_output = stdout[xml_start:]
|
||||
hosts = parse_nmap_xml(xml_output)
|
||||
if hosts:
|
||||
return hosts
|
||||
except Exception as e:
|
||||
print(f"XML parsing failed: {e}")
|
||||
|
||||
# Fallback to text parsing
|
||||
hosts = parse_nmap_text(stdout)
|
||||
return hosts
|
||||
|
||||
|
||||
def parse_nmap_xml(xml_output: str) -> List[Dict[str, Any]]:
|
||||
"""Parse nmap XML output to extract hosts."""
|
||||
hosts = []
|
||||
try:
|
||||
root = ET.fromstring(xml_output)
|
||||
|
||||
for host_elem in root.findall('.//host'):
|
||||
status = host_elem.find("status")
|
||||
if status is None or status.get("state") != "up":
|
||||
continue
|
||||
|
||||
host = {
|
||||
"ip": "",
|
||||
"hostname": "",
|
||||
"mac": "",
|
||||
"vendor": "",
|
||||
"os_type": "",
|
||||
"os_details": "",
|
||||
"ports": []
|
||||
}
|
||||
|
||||
# Get IP address
|
||||
addr = host_elem.find("address[@addrtype='ipv4']")
|
||||
if addr is not None:
|
||||
host["ip"] = addr.get("addr", "")
|
||||
|
||||
# Get MAC address
|
||||
mac = host_elem.find("address[@addrtype='mac']")
|
||||
if mac is not None:
|
||||
host["mac"] = mac.get("addr", "")
|
||||
host["vendor"] = mac.get("vendor", "")
|
||||
|
||||
# Get hostname
|
||||
hostname = host_elem.find(".//hostname")
|
||||
if hostname is not None:
|
||||
host["hostname"] = hostname.get("name", "")
|
||||
|
||||
# Get OS info
|
||||
os_elem = host_elem.find(".//osmatch")
|
||||
if os_elem is not None:
|
||||
os_name = os_elem.get("name", "")
|
||||
host["os_details"] = os_name
|
||||
host["os_type"] = detect_os_type(os_name)
|
||||
|
||||
# Get ports
|
||||
for port_elem in host_elem.findall(".//port"):
|
||||
state_elem = port_elem.find("state")
|
||||
port_info = {
|
||||
"port": int(port_elem.get("portid", 0)),
|
||||
"protocol": port_elem.get("protocol", "tcp"),
|
||||
"state": state_elem.get("state", "") if state_elem is not None else "",
|
||||
"service": ""
|
||||
}
|
||||
service = port_elem.find("service")
|
||||
if service is not None:
|
||||
port_info["service"] = service.get("name", "")
|
||||
port_info["product"] = service.get("product", "")
|
||||
port_info["version"] = service.get("version", "")
|
||||
|
||||
if port_info["state"] == "open":
|
||||
host["ports"].append(port_info)
|
||||
|
||||
# Infer OS from ports if still unknown
|
||||
if not host["os_type"] and host["ports"]:
|
||||
host["os_type"] = infer_os_from_ports(host["ports"])
|
||||
|
||||
# Only include hosts with at least one OPEN port
|
||||
# This prevents false positives from proxy ARP responses
|
||||
# where routers respond for all IPs even if device is offline
|
||||
if host["ip"] and host["ports"]:
|
||||
hosts.append(host)
|
||||
|
||||
except ET.ParseError as e:
|
||||
print(f"XML parse error: {e}")
|
||||
|
||||
return hosts
|
||||
|
||||
|
||||
def parse_nmap_text(output: str) -> List[Dict[str, Any]]:
|
||||
"""Parse nmap text output as fallback.
|
||||
|
||||
Only returns hosts that have at least one OPEN port.
|
||||
Filters out false positives from router proxy ARP (where all IPs appear "up").
|
||||
"""
|
||||
hosts = []
|
||||
current_host = None
|
||||
|
||||
def save_host_if_has_open_ports(host):
|
||||
"""Only save host if it has at least one open port."""
|
||||
if host and host.get("ip") and host.get("ports"):
|
||||
# Infer OS before saving
|
||||
if not host["os_type"]:
|
||||
host["os_type"] = infer_os_from_ports(host["ports"])
|
||||
hosts.append(host)
|
||||
|
||||
for line in output.split('\n'):
|
||||
# Match host line: "Nmap scan report for hostname (IP)" or "Nmap scan report for IP"
|
||||
host_match = re.search(r'Nmap scan report for (?:(\S+) \()?(\d+\.\d+\.\d+\.\d+)', line)
|
||||
if host_match:
|
||||
# Save previous host only if it has open ports
|
||||
save_host_if_has_open_ports(current_host)
|
||||
|
||||
current_host = {
|
||||
"ip": host_match.group(2),
|
||||
"hostname": host_match.group(1) or "",
|
||||
"os_type": "",
|
||||
"os_details": "",
|
||||
"ports": [],
|
||||
"mac": "",
|
||||
"vendor": ""
|
||||
}
|
||||
continue
|
||||
|
||||
if current_host:
|
||||
# Match MAC: "MAC Address: XX:XX:XX:XX:XX:XX (Vendor Name)"
|
||||
mac_match = re.search(r'MAC Address: ([0-9A-Fa-f:]+)(?: \(([^)]+)\))?', line)
|
||||
if mac_match:
|
||||
current_host["mac"] = mac_match.group(1)
|
||||
current_host["vendor"] = mac_match.group(2) or ""
|
||||
|
||||
# Match port: "80/tcp open http Apache httpd"
|
||||
port_match = re.search(r'(\d+)/(tcp|udp)\s+(\w+)\s+(\S+)(?:\s+(.*))?', line)
|
||||
if port_match and port_match.group(3) == "open":
|
||||
port_info = {
|
||||
"port": int(port_match.group(1)),
|
||||
"protocol": port_match.group(2),
|
||||
"state": "open",
|
||||
"service": port_match.group(4),
|
||||
"product": port_match.group(5) or ""
|
||||
}
|
||||
current_host["ports"].append(port_info)
|
||||
|
||||
# Match OS: "OS details: Linux 4.15 - 5.6" or "Running: Linux"
|
||||
os_match = re.search(r'(?:OS details?|Running):\s*(.+)', line)
|
||||
if os_match:
|
||||
current_host["os_details"] = os_match.group(1)
|
||||
current_host["os_type"] = detect_os_type(os_match.group(1))
|
||||
|
||||
# Match "Service Info: OS: Linux" style
|
||||
service_os_match = re.search(r'Service Info:.*OS:\s*([^;,]+)', line)
|
||||
if service_os_match and not current_host["os_type"]:
|
||||
current_host["os_type"] = detect_os_type(service_os_match.group(1))
|
||||
|
||||
# Match "Aggressive OS guesses: Linux 5.4 (98%)" - take first high confidence
|
||||
aggressive_match = re.search(r'Aggressive OS guesses:\s*([^(]+)\s*\((\d+)%\)', line)
|
||||
if aggressive_match and not current_host["os_details"]:
|
||||
confidence = int(aggressive_match.group(2))
|
||||
if confidence >= 85:
|
||||
current_host["os_details"] = aggressive_match.group(1).strip()
|
||||
current_host["os_type"] = detect_os_type(aggressive_match.group(1))
|
||||
|
||||
# Don't forget the last host - only if it has open ports
|
||||
save_host_if_has_open_ports(current_host)
|
||||
|
||||
return hosts
|
||||
|
||||
|
||||
async def send_hosts_to_dashboard(hosts: List[Dict[str, Any]]):
|
||||
"""Send discovered hosts to the dashboard for network map update."""
|
||||
if not hosts:
|
||||
return
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.post(
|
||||
f"{DASHBOARD_URL}/api/network/hosts/discover",
|
||||
json={"hosts": hosts, "source": "terminal"}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print(f"Sent {len(hosts)} hosts to dashboard: added={result.get('added')}, updated={result.get('updated')}")
|
||||
else:
|
||||
print(f"Failed to send hosts to dashboard: {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f"Error sending hosts to dashboard: {e}")
|
||||
|
||||
|
||||
# Docker client
|
||||
docker_client = None
|
||||
kali_container = None
|
||||
@@ -253,6 +557,23 @@ def _run_command_sync(container, command, working_dir):
|
||||
workdir=working_dir
|
||||
)
|
||||
|
||||
@app.get("/stream/processes")
|
||||
async def stream_running_processes():
|
||||
"""Server-Sent Events stream of running security processes.
|
||||
Emits JSON events with current process list every 5 seconds.
|
||||
"""
|
||||
|
||||
async def event_generator():
|
||||
while True:
|
||||
try:
|
||||
data = await get_running_processes()
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
except Exception as e:
|
||||
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
||||
await asyncio.sleep(5)
|
||||
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
|
||||
@app.post("/execute", response_model=CommandResult)
|
||||
async def execute_command(request: CommandRequest):
|
||||
"""Execute a command in the Kali container."""
|
||||
@@ -292,6 +613,15 @@ async def execute_command(request: CommandRequest):
|
||||
stdout = output[0].decode('utf-8', errors='replace') if output[0] else ""
|
||||
stderr = output[1].decode('utf-8', errors='replace') if output[1] else ""
|
||||
|
||||
# Parse nmap output and send hosts to dashboard for network map
|
||||
if is_nmap_command(request.command) and stdout:
|
||||
try:
|
||||
hosts = parse_nmap_output(stdout)
|
||||
if hosts:
|
||||
asyncio.create_task(send_hosts_to_dashboard(hosts))
|
||||
except Exception as e:
|
||||
print(f"Error parsing nmap output: {e}")
|
||||
|
||||
return CommandResult(
|
||||
command_id=command_id,
|
||||
command=request.command,
|
||||
@@ -309,6 +639,52 @@ async def execute_command(request: CommandRequest):
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Execution error: {str(e)}")
|
||||
|
||||
@app.websocket("/ws/execute/{command_id}")
|
||||
async def websocket_execute(websocket: WebSocket, command_id: str):
|
||||
"""WebSocket endpoint for streaming command output in real-time."""
|
||||
await websocket.accept()
|
||||
|
||||
if command_id not in running_commands:
|
||||
await websocket.send_json({"error": "Command not found"})
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
cmd_info = running_commands[command_id]
|
||||
|
||||
try:
|
||||
# Stream output as it becomes available
|
||||
last_stdout_len = 0
|
||||
last_stderr_len = 0
|
||||
|
||||
while cmd_info["status"] == "running":
|
||||
current_stdout = cmd_info.get("stdout", "")
|
||||
current_stderr = cmd_info.get("stderr", "")
|
||||
|
||||
# Send new stdout
|
||||
if len(current_stdout) > last_stdout_len:
|
||||
new_stdout = current_stdout[last_stdout_len:]
|
||||
await websocket.send_json({"type": "stdout", "data": new_stdout})
|
||||
last_stdout_len = len(current_stdout)
|
||||
|
||||
# Send new stderr
|
||||
if len(current_stderr) > last_stderr_len:
|
||||
new_stderr = current_stderr[last_stderr_len:]
|
||||
await websocket.send_json({"type": "stderr", "data": new_stderr})
|
||||
last_stderr_len = len(current_stderr)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Send final status
|
||||
await websocket.send_json({
|
||||
"type": "complete",
|
||||
"status": cmd_info["status"],
|
||||
"exit_code": cmd_info.get("exit_code"),
|
||||
"duration": cmd_info.get("duration_seconds"),
|
||||
})
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
await websocket.close()
|
||||
|
||||
@app.post("/execute/async")
|
||||
async def execute_command_async(request: CommandRequest):
|
||||
@@ -420,18 +796,92 @@ async def websocket_execute(websocket: WebSocket):
|
||||
workdir=working_dir
|
||||
)
|
||||
|
||||
# Stream output
|
||||
for stdout, stderr in exec_result.output:
|
||||
if stdout:
|
||||
await websocket.send_json({
|
||||
"type": "stdout",
|
||||
"data": stdout.decode('utf-8', errors='replace')
|
||||
})
|
||||
if stderr:
|
||||
await websocket.send_json({
|
||||
"type": "stderr",
|
||||
"data": stderr.decode('utf-8', errors='replace')
|
||||
})
|
||||
# Collect output for nmap parsing
|
||||
full_stdout = []
|
||||
is_nmap = is_nmap_command(command)
|
||||
|
||||
# Stream output with keepalive for long-running commands
|
||||
last_output_time = asyncio.get_event_loop().time()
|
||||
output_queue = asyncio.Queue()
|
||||
stream_complete = asyncio.Event()
|
||||
|
||||
# Synchronous function to read from Docker stream (runs in thread)
|
||||
def read_docker_output_sync(queue: asyncio.Queue, loop, complete_event):
|
||||
try:
|
||||
for stdout, stderr in exec_result.output:
|
||||
if stdout:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
queue.put(("stdout", stdout.decode('utf-8', errors='replace'))),
|
||||
loop
|
||||
)
|
||||
if stderr:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
queue.put(("stderr", stderr.decode('utf-8', errors='replace'))),
|
||||
loop
|
||||
)
|
||||
except Exception as e:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
queue.put(("error", str(e))),
|
||||
loop
|
||||
)
|
||||
finally:
|
||||
loop.call_soon_threadsafe(complete_event.set)
|
||||
|
||||
# Start reading in background thread
|
||||
loop = asyncio.get_event_loop()
|
||||
read_future = loop.run_in_executor(
|
||||
executor,
|
||||
read_docker_output_sync,
|
||||
output_queue,
|
||||
loop,
|
||||
stream_complete
|
||||
)
|
||||
|
||||
# Send output and keepalives
|
||||
keepalive_interval = 25 # seconds
|
||||
while not stream_complete.is_set() or not output_queue.empty():
|
||||
try:
|
||||
# Wait for output with timeout for keepalive
|
||||
try:
|
||||
msg_type, msg_data = await asyncio.wait_for(
|
||||
output_queue.get(),
|
||||
timeout=keepalive_interval
|
||||
)
|
||||
last_output_time = asyncio.get_event_loop().time()
|
||||
|
||||
if msg_type == "stdout":
|
||||
if is_nmap:
|
||||
full_stdout.append(msg_data)
|
||||
await websocket.send_json({"type": "stdout", "data": msg_data})
|
||||
elif msg_type == "stderr":
|
||||
await websocket.send_json({"type": "stderr", "data": msg_data})
|
||||
elif msg_type == "error":
|
||||
await websocket.send_json({"type": "error", "message": msg_data})
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
# No output for a while, send keepalive
|
||||
elapsed = asyncio.get_event_loop().time() - last_output_time
|
||||
await websocket.send_json({
|
||||
"type": "keepalive",
|
||||
"elapsed": int(elapsed),
|
||||
"message": f"Scan in progress ({int(elapsed)}s)..."
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error in output loop: {e}")
|
||||
break
|
||||
|
||||
# Wait for read thread to complete
|
||||
await read_future
|
||||
|
||||
# Parse nmap output and send hosts to dashboard
|
||||
if is_nmap and full_stdout:
|
||||
try:
|
||||
combined_output = "".join(full_stdout)
|
||||
hosts = parse_nmap_output(combined_output)
|
||||
if hosts:
|
||||
asyncio.create_task(send_hosts_to_dashboard(hosts))
|
||||
except Exception as e:
|
||||
print(f"Error parsing nmap output: {e}")
|
||||
|
||||
await websocket.send_json({
|
||||
"type": "complete",
|
||||
|
||||
@@ -3,3 +3,4 @@ uvicorn[standard]==0.32.1
|
||||
docker==7.1.0
|
||||
pydantic==2.10.2
|
||||
websockets==14.1
|
||||
httpx==0.28.1
|
||||
|
||||
@@ -3,15 +3,24 @@ FROM kalilinux/kali-rolling
|
||||
# Avoid prompts during package installation
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Update and install ALL Kali tools
|
||||
# Using kali-linux-everything metapackage for complete tool suite
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
kali-linux-everything \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# Configure apt to use reliable mirrors and retry on failure
|
||||
RUN echo 'Acquire::Retries "3";' > /etc/apt/apt.conf.d/80-retries && \
|
||||
echo 'Acquire::http::Timeout "30";' >> /etc/apt/apt.conf.d/80-retries && \
|
||||
echo 'deb http://kali.download/kali kali-rolling main non-free non-free-firmware contrib' > /etc/apt/sources.list
|
||||
|
||||
# Install kali-linux-everything metapackage (600+ tools, ~15GB)
|
||||
# This includes: nmap, metasploit, burpsuite, wireshark, aircrack-ng,
|
||||
# hashcat, john, hydra, sqlmap, nikto, wpscan, responder, crackmapexec,
|
||||
# enum4linux, gobuster, dirb, wfuzz, masscan, and hundreds more
|
||||
RUN apt-get update && \
|
||||
apt-get install -y kali-linux-everything && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install additional Python tools and utilities for command logging
|
||||
RUN pip3 install --break-system-packages \
|
||||
# Install setuptools first to fix compatibility issues with Python 3.13
|
||||
RUN pip3 install --break-system-packages setuptools wheel && \
|
||||
pip3 install --break-system-packages \
|
||||
requests \
|
||||
beautifulsoup4 \
|
||||
shodan \
|
||||
@@ -27,11 +36,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
# Create workspace directory
|
||||
WORKDIR /workspace
|
||||
|
||||
# Copy scripts
|
||||
# Copy scripts and fix line endings (in case of Windows CRLF)
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
COPY command_logger.sh /usr/local/bin/command_logger.sh
|
||||
COPY capture_wrapper.sh /usr/local/bin/capture
|
||||
RUN chmod +x /entrypoint.sh /usr/local/bin/command_logger.sh /usr/local/bin/capture
|
||||
RUN sed -i 's/\r$//' /entrypoint.sh /usr/local/bin/command_logger.sh /usr/local/bin/capture && \
|
||||
chmod +x /entrypoint.sh /usr/local/bin/command_logger.sh /usr/local/bin/capture
|
||||
|
||||
# Create command history directory
|
||||
RUN mkdir -p /workspace/.command_history
|
||||
|
||||
Reference in New Issue
Block a user