Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d5a4d4e20 | |||
| 26bcb7f947 | |||
|
|
91b4697403 | ||
|
|
c4eaf1718a | ||
|
|
aa64383530 | ||
|
|
4028c6326e | ||
|
|
4e3cf99e04 | ||
|
|
d0aadefad9 | ||
|
|
70fb291bf1 | ||
|
|
c5a2741c90 | ||
|
|
fe6b3fa373 | ||
|
|
f49b63e7af | ||
|
|
7b75477450 | ||
|
|
193cb42aef | ||
| 08d604ba38 | |||
| 667021b275 | |||
| ff1b0600a2 | |||
| 4a6c613e28 | |||
| c5e60476e2 |
@@ -7,3 +7,10 @@ ANTHROPIC_API_KEY=
|
||||
|
||||
# Ollama Configuration
|
||||
OLLAMA_BASE_URL=http://ollama:11434
|
||||
|
||||
# Default LLM Provider and Model
|
||||
# These are used when no explicit provider/model is specified in API requests
|
||||
# Can be changed via API: POST /api/llm/preferences
|
||||
DEFAULT_LLM_PROVIDER=ollama
|
||||
DEFAULT_LLM_MODEL=llama3.2
|
||||
# Available providers: ollama, ollama-local, ollama-network, openai, anthropic
|
||||
|
||||
119
.github/workflows/unpack-zip.yml
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
name: Unpack files.zip (create branch + PR)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'Branch containing files.zip'
|
||||
required: true
|
||||
default: 'C2-integration'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
unpack-and-pr:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# ---------------------------------------------------------
|
||||
# 0. Checkout the target branch ONLY — prevents recursion
|
||||
# ---------------------------------------------------------
|
||||
- name: Checkout target branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y unzip rsync jq
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 1. Verify files.zip exists in branch root
|
||||
# ---------------------------------------------------------
|
||||
- name: Check for files.zip
|
||||
run: |
|
||||
if [ ! -f "files.zip" ]; then
|
||||
echo "::error ::files.zip not found in root of branch ${{ github.event.inputs.branch }}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found files.zip:"
|
||||
ls -lh files.zip
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 2. Unzip files into extracted/
|
||||
# ---------------------------------------------------------
|
||||
- name: Extract zip
|
||||
run: |
|
||||
rm -rf extracted
|
||||
mkdir extracted
|
||||
unzip -o files.zip -d extracted
|
||||
echo "Extracted files sample:"
|
||||
find extracted -type f | sed -n '1,50p'
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 3. Copy extracted files into root of repo
|
||||
# ---------------------------------------------------------
|
||||
- name: Copy extracted contents
|
||||
run: |
|
||||
rsync -a extracted/ . --exclude='.git'
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 4. Detect changes and create commit branch
|
||||
# ---------------------------------------------------------
|
||||
- name: Commit changes if any
|
||||
id: gitops
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
if git status --porcelain | grep -q . ; then
|
||||
BRANCH="unpacked-${{ github.event.inputs.branch }}-$(date +%s)"
|
||||
git checkout -b "$BRANCH"
|
||||
git add -A
|
||||
git commit -m "Unpacked files.zip automatically"
|
||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "nochanges=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 5. Push branch only if changes exist
|
||||
# ---------------------------------------------------------
|
||||
- name: Push branch
|
||||
if: steps.gitops.outputs.nochanges != 'true'
|
||||
run: |
|
||||
git push --set-upstream origin "${{ steps.gitops.outputs.branch }}"
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 6. Open PR only if changes exist
|
||||
# ---------------------------------------------------------
|
||||
- name: Open Pull Request
|
||||
if: steps.gitops.outputs.nochanges != 'true'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
title: "Automated unpack of files.zip into ${{ github.event.inputs.branch }}"
|
||||
body: |
|
||||
This PR was automatically generated.
|
||||
|
||||
**Action:** Unpacked `files.zip` from branch `${{ github.event.inputs.branch }}`.
|
||||
**Branch:** `${{ steps.gitops.outputs.branch }}`
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
head: ${{ steps.gitops.outputs.branch }}
|
||||
draft: false
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 7. Final log
|
||||
# ---------------------------------------------------------
|
||||
- name: Done
|
||||
run: |
|
||||
if [ "${{ steps.gitops.outputs.nochanges }}" = "true" ]; then
|
||||
echo "No changes detected. Nothing to commit."
|
||||
else
|
||||
echo "PR created successfully."
|
||||
fi
|
||||
405
BIDIRECTIONAL_CAPTURE.md
Normal file
@@ -0,0 +1,405 @@
|
||||
# Bidirectional Command Capture
|
||||
|
||||
## Overview
|
||||
|
||||
StrikePackageGPT now supports **bidirectional command capture**, enabling commands run directly in the Kali container to be automatically captured and displayed in the dashboard alongside commands executed via the UI/API.
|
||||
|
||||
This feature is perfect for advanced users who prefer command-line interfaces but still want visual tracking and historical reference.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Two-Way Flow │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Dashboard UI → HackGPT API → Kali Executor → Kali Container│
|
||||
│ ↓ ↑ │
|
||||
│ Stored in scan_results ←──────────────────────── │
|
||||
│ ↓ │
|
||||
│ Displayed in Dashboard History │
|
||||
│ │
|
||||
│ Direct Shell → Command Logger → JSON Files → API Sync │
|
||||
│ ↓ ↑ │
|
||||
│ /workspace/.command_history Auto-Import │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Automatic Logging
|
||||
- All commands run in interactive bash sessions are automatically logged
|
||||
- Command metadata captured: timestamp, user, working directory, exit code, duration
|
||||
- Full stdout/stderr captured for commands run with `capture` wrapper
|
||||
|
||||
### Unified History
|
||||
- Commands from both sources (UI and direct shell) appear in the same history
|
||||
- Consistent format and parsing across all command sources
|
||||
- Network visualization includes manually-run scans
|
||||
|
||||
### Real-Time Sync
|
||||
- API endpoint to pull latest captured commands
|
||||
- Background sync every 30 seconds (configurable)
|
||||
- Manual sync available via `/commands/sync` endpoint
|
||||
|
||||
## Usage
|
||||
|
||||
### Option 1: Automatic Logging (All Commands)
|
||||
|
||||
When you connect to the Kali container, command logging is enabled automatically:
|
||||
|
||||
```bash
|
||||
docker exec -it strikepackage-kali bash
|
||||
```
|
||||
|
||||
Now run any security tool:
|
||||
|
||||
```bash
|
||||
nmap -sV 192.168.1.0/24
|
||||
sqlmap -u "http://example.com?id=1"
|
||||
nikto -h http://example.com
|
||||
```
|
||||
|
||||
These commands are logged with basic metadata. Full output capture requires Option 2.
|
||||
|
||||
### Option 2: Explicit Capture (With Full Output)
|
||||
|
||||
Use the `capture` command prefix for full output capture:
|
||||
|
||||
```bash
|
||||
docker exec -it strikepackage-kali bash
|
||||
capture nmap -sV 192.168.1.0/24
|
||||
capture gobuster dir -u http://example.com -w /usr/share/wordlists/dirb/common.txt
|
||||
```
|
||||
|
||||
This captures:
|
||||
- Full stdout and stderr
|
||||
- Exit codes
|
||||
- Execution duration
|
||||
- All command metadata
|
||||
|
||||
### View Recent Commands
|
||||
|
||||
Inside the container:
|
||||
|
||||
```bash
|
||||
recent # Shows last 10 captured commands
|
||||
```
|
||||
|
||||
### Sync to Dashboard
|
||||
|
||||
Commands are automatically synced to the dashboard. To manually trigger a sync:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8001/commands/sync
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Get Captured Commands
|
||||
|
||||
```bash
|
||||
GET /commands/captured?limit=50&since=2025-12-03T00:00:00Z
|
||||
```
|
||||
|
||||
Returns commands captured from interactive sessions.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"command_id": "abc-123-def",
|
||||
"command": "nmap -sV 192.168.1.0/24",
|
||||
"timestamp": "2025-12-03T14:30:00Z",
|
||||
"completed_at": "2025-12-03T14:35:00Z",
|
||||
"status": "completed",
|
||||
"exit_code": 0,
|
||||
"duration": 300,
|
||||
"stdout": "... nmap output ...",
|
||||
"stderr": "",
|
||||
"user": "root",
|
||||
"working_dir": "/workspace",
|
||||
"source": "capture_wrapper"
|
||||
}
|
||||
],
|
||||
"count": 1,
|
||||
"imported_to_history": true
|
||||
}
|
||||
```
|
||||
|
||||
### Sync Commands to History
|
||||
|
||||
```bash
|
||||
POST /commands/sync
|
||||
```
|
||||
|
||||
Imports all captured commands into the unified scan history, making them visible in the dashboard.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "synced",
|
||||
"imported_count": 15,
|
||||
"message": "All captured commands are now visible in dashboard history"
|
||||
}
|
||||
```
|
||||
|
||||
### View Unified History
|
||||
|
||||
```bash
|
||||
GET /scans
|
||||
```
|
||||
|
||||
Returns all commands from both sources (UI and direct shell).
|
||||
|
||||
## Dashboard Integration
|
||||
|
||||
### Viewing Captured Commands
|
||||
|
||||
1. **Scan History Tab**: Shows all commands (UI + captured)
|
||||
2. **Network Map**: Includes hosts discovered via manual scans
|
||||
3. **Timeline View**: Shows when commands were executed
|
||||
4. **Filter by Source**: Filter to show only manually-run or UI-run commands
|
||||
|
||||
### Visual Indicators
|
||||
|
||||
- 🔷 **UI Commands**: Blue indicator
|
||||
- 🔶 **Manual Commands**: Orange indicator with "Interactive Shell" badge
|
||||
- ⚡ **Running**: Animated indicator
|
||||
- ✅ **Completed**: Green checkmark
|
||||
- ❌ **Failed**: Red X with error details
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable/Disable Automatic Logging
|
||||
|
||||
To disable automatic logging in new shell sessions:
|
||||
|
||||
```bash
|
||||
# Inside container
|
||||
echo 'DISABLE_AUTO_LOGGING=1' >> ~/.bashrc
|
||||
```
|
||||
|
||||
### Change Log Directory
|
||||
|
||||
Set a custom log directory:
|
||||
|
||||
```bash
|
||||
# In docker-compose.yml or .env
|
||||
COMMAND_LOG_DIR=/custom/path/.command_history
|
||||
```
|
||||
|
||||
### Sync Interval
|
||||
|
||||
Configure auto-sync interval (default: 30 seconds):
|
||||
|
||||
```bash
|
||||
# In HackGPT API configuration
|
||||
COMMAND_SYNC_INTERVAL=60 # seconds
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Storage Format
|
||||
|
||||
Commands are stored as JSON files in `/workspace/.command_history/`:
|
||||
|
||||
```json
|
||||
{
|
||||
"command_id": "unique-uuid",
|
||||
"command": "nmap -sV 192.168.1.1",
|
||||
"timestamp": "2025-12-03T14:30:00Z",
|
||||
"completed_at": "2025-12-03T14:35:00Z",
|
||||
"user": "root",
|
||||
"working_dir": "/workspace",
|
||||
"source": "capture_wrapper",
|
||||
"status": "completed",
|
||||
"exit_code": 0,
|
||||
"duration": 300,
|
||||
"stdout": "...",
|
||||
"stderr": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Command Logger (`command_logger.sh`)
|
||||
|
||||
- Hooks into `PROMPT_COMMAND` for automatic logging
|
||||
- Filters out basic commands (cd, ls, etc.)
|
||||
- Lightweight metadata-only logging
|
||||
- No performance impact on command execution
|
||||
|
||||
### Capture Wrapper (`capture`)
|
||||
|
||||
- Full command wrapper for complete output capture
|
||||
- Uses `eval` with output redirection
|
||||
- Measures execution time
|
||||
- Captures exit codes
|
||||
- Saves results as JSON
|
||||
|
||||
### API Integration
|
||||
|
||||
1. **Kali Executor** reads JSON files from `/workspace/.command_history/`
|
||||
2. **HackGPT API** imports them into `scan_results` dict
|
||||
3. **Dashboard** displays them alongside UI-initiated commands
|
||||
4. Automatic deduplication prevents duplicates
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Command Whitelist
|
||||
|
||||
- Command logging respects the existing whitelist
|
||||
- Only whitelisted tools are executed
|
||||
- Malicious commands are blocked before logging
|
||||
|
||||
### Storage Limits
|
||||
|
||||
- Log directory is size-limited (default: 10MB)
|
||||
- Oldest logs are automatically purged
|
||||
- Configurable retention period
|
||||
|
||||
### Access Control
|
||||
|
||||
- Logs are stored in container-specific workspace
|
||||
- Only accessible via API with authentication (when enabled)
|
||||
- No cross-container access
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Commands Not Appearing in Dashboard
|
||||
|
||||
1. **Check logging is enabled**:
|
||||
```bash
|
||||
docker exec -it strikepackage-kali bash -c 'echo $PROMPT_COMMAND'
|
||||
```
|
||||
|
||||
2. **Verify log files are created**:
|
||||
```bash
|
||||
docker exec -it strikepackage-kali ls -la /workspace/.command_history/
|
||||
```
|
||||
|
||||
3. **Manually trigger sync**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8001/commands/sync
|
||||
```
|
||||
|
||||
### Output Not Captured
|
||||
|
||||
- Use `capture` prefix for full output: `capture nmap ...`
|
||||
- Check log file exists: `ls /workspace/.command_history/`
|
||||
- Verify command completed: `recent`
|
||||
|
||||
### Performance Issues
|
||||
|
||||
If logging causes slowdowns:
|
||||
|
||||
1. **Disable for current session**:
|
||||
```bash
|
||||
unset PROMPT_COMMAND
|
||||
```
|
||||
|
||||
2. **Increase sync interval**:
|
||||
```bash
|
||||
# In .env
|
||||
COMMAND_SYNC_INTERVAL=120
|
||||
```
|
||||
|
||||
3. **Clear old logs**:
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8001/captured_commands/clear
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Network Reconnaissance
|
||||
|
||||
```bash
|
||||
# In Kali container
|
||||
docker exec -it strikepackage-kali bash
|
||||
|
||||
# Run discovery scan (automatically logged)
|
||||
nmap -sn 192.168.1.0/24
|
||||
|
||||
# Run detailed scan with full capture
|
||||
capture nmap -sV -sC -p- 192.168.1.100
|
||||
|
||||
# View in dashboard
|
||||
# → Go to Scan History
|
||||
# → See both commands with full results
|
||||
# → View in Network Map
|
||||
```
|
||||
|
||||
### Example 2: Web Application Testing
|
||||
|
||||
```bash
|
||||
# Directory bruteforce
|
||||
capture gobuster dir -u http://target.com -w /usr/share/wordlists/dirb/common.txt
|
||||
|
||||
# SQL injection testing
|
||||
capture sqlmap -u "http://target.com?id=1" --batch --dbs
|
||||
|
||||
# Vulnerability scanning
|
||||
capture nikto -h http://target.com
|
||||
|
||||
# All results appear in dashboard history
|
||||
```
|
||||
|
||||
### Example 3: Wireless Auditing
|
||||
|
||||
```bash
|
||||
# Put adapter in monitor mode
|
||||
capture airmon-ng start wlan0
|
||||
|
||||
# Scan for networks
|
||||
capture airodump-ng wlan0mon
|
||||
|
||||
# Results visible in dashboard with timestamps
|
||||
```
|
||||
|
||||
## Advantages
|
||||
|
||||
### For Advanced Users
|
||||
- ✅ Use familiar command-line interface
|
||||
- ✅ Full control over tool parameters
|
||||
- ✅ Faster than clicking through UI
|
||||
- ✅ Still get visual tracking and history
|
||||
|
||||
### For Teams
|
||||
- ✅ All team member activity captured
|
||||
- ✅ Unified view of all scan activity
|
||||
- ✅ Easy to review what was run
|
||||
- ✅ Share results without screenshots
|
||||
|
||||
### For Reporting
|
||||
- ✅ Complete audit trail
|
||||
- ✅ Timestamp all activities
|
||||
- ✅ Include in final reports
|
||||
- ✅ Demonstrate thoroughness
|
||||
|
||||
## Comparison
|
||||
|
||||
| Feature | UI-Only | Bidirectional |
|
||||
|---------|---------|---------------|
|
||||
| Run commands via dashboard | ✅ | ✅ |
|
||||
| Run commands via CLI | ❌ | ✅ |
|
||||
| Visual history | ✅ | ✅ |
|
||||
| Network map integration | ✅ | ✅ |
|
||||
| Advanced tool parameters | Limited | Full |
|
||||
| Speed for power users | Slow | Fast |
|
||||
| Learning curve | Low | Medium |
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Real-time streaming**: See command output as it runs
|
||||
- **Collaborative mode**: Multiple users see each other's commands
|
||||
- **Smart suggestions**: AI suggests next commands based on results
|
||||
- **Template library**: Save common command sequences
|
||||
- **Report integration**: One-click add to PDF report
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- GitHub Issues: https://github.com/mblanke/StrikePackageGPT/issues
|
||||
- Documentation: See `FEATURES.md` and `INTEGRATION_EXAMPLE.md`
|
||||
- Examples: Check `examples/` directory
|
||||
878
FEATURES.md
Normal file
@@ -0,0 +1,878 @@
|
||||
# StrikePackageGPT - New Features Documentation
|
||||
|
||||
This document describes the newly added features to StrikePackageGPT, including voice control, interactive network mapping, beginner onboarding, LLM-driven help, and workflow integration.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Backend Modules](#backend-modules)
|
||||
2. [Frontend Components](#frontend-components)
|
||||
3. [API Endpoints](#api-endpoints)
|
||||
4. [Setup & Configuration](#setup--configuration)
|
||||
5. [Usage Examples](#usage-examples)
|
||||
6. [Integration Guide](#integration-guide)
|
||||
|
||||
---
|
||||
|
||||
## Backend Modules
|
||||
|
||||
### 1. Nmap Parser (`nmap_parser.py`)
|
||||
|
||||
**Purpose:** Parse Nmap XML or JSON output to extract detailed host information.
|
||||
|
||||
**Features:**
|
||||
- Parse Nmap XML and JSON formats
|
||||
- Extract IP addresses, hostnames, OS detection
|
||||
- Device type classification (server, workstation, network device, etc.)
|
||||
- MAC address and vendor information
|
||||
- Port and service enumeration
|
||||
- OS icon recommendations
|
||||
|
||||
**Functions:**
|
||||
```python
|
||||
parse_nmap_xml(xml_content: str) -> List[Dict[str, Any]]
|
||||
parse_nmap_json(json_content: str) -> List[Dict[str, Any]]
|
||||
classify_device_type(host: Dict) -> str
|
||||
detect_os_type(os_string: str) -> str
|
||||
get_os_icon_name(host: Dict) -> str
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```python
|
||||
from app import nmap_parser
|
||||
|
||||
# Parse XML output
|
||||
with open('nmap_scan.xml', 'r') as f:
|
||||
xml_data = f.read()
|
||||
hosts = nmap_parser.parse_nmap_xml(xml_data)
|
||||
|
||||
for host in hosts:
|
||||
print(f"IP: {host['ip']}, OS: {host['os_type']}, Device: {host['device_type']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Voice Control (`voice.py`)
|
||||
|
||||
**Purpose:** Speech-to-text, text-to-speech, and voice command routing.
|
||||
|
||||
**Features:**
|
||||
- Speech-to-text using local Whisper (preferred) or OpenAI API
|
||||
- Text-to-speech using OpenAI TTS, Coqui TTS, or browser fallback
|
||||
- Voice command parsing and routing
|
||||
- Support for common commands: list, scan, deploy, status, help
|
||||
|
||||
**Functions:**
|
||||
```python
|
||||
transcribe_audio(audio_data: bytes, format: str = "wav") -> Dict[str, Any]
|
||||
speak_text(text: str, voice: str = "alloy") -> Optional[bytes]
|
||||
parse_voice_command(text: str) -> Dict[str, Any]
|
||||
route_command(command_result: Dict) -> Dict[str, Any]
|
||||
get_voice_command_help() -> Dict[str, list]
|
||||
```
|
||||
|
||||
**Supported Commands:**
|
||||
- "Scan 192.168.1.1"
|
||||
- "List scans"
|
||||
- "Show agents"
|
||||
- "Deploy agent on target.com"
|
||||
- "What's the status"
|
||||
- "Help me with nmap"
|
||||
|
||||
**Configuration:**
|
||||
```bash
|
||||
# Optional: For local Whisper
|
||||
pip install openai-whisper
|
||||
|
||||
# Optional: For OpenAI API
|
||||
export OPENAI_API_KEY=sk-...
|
||||
|
||||
# Optional: For Coqui TTS
|
||||
pip install TTS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Explain Module (`explain.py`)
|
||||
|
||||
**Purpose:** Plain-English explanations for configs, logs, and errors.
|
||||
|
||||
**Features:**
|
||||
- Configuration explanations with recommendations
|
||||
- Error message interpretation with suggested fixes
|
||||
- Log entry analysis with severity assessment
|
||||
- Wizard step help for onboarding
|
||||
- Auto-fix suggestions
|
||||
|
||||
**Functions:**
|
||||
```python
|
||||
explain_config(config_key: str, config_value: Any, context: Optional[Dict]) -> Dict
|
||||
explain_error(error_message: str, error_type: Optional[str], context: Optional[Dict]) -> Dict
|
||||
explain_log_entry(log_entry: str, log_level: Optional[str]) -> Dict
|
||||
get_wizard_step_help(wizard_type: str, step_number: int) -> Dict
|
||||
suggest_fix(issue_description: str, context: Optional[Dict]) -> List[str]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
from app import explain
|
||||
|
||||
# Explain a config setting
|
||||
result = explain.explain_config("timeout", 30)
|
||||
print(result['description'])
|
||||
print(result['recommendations'])
|
||||
|
||||
# Explain an error
|
||||
result = explain.explain_error("Connection refused")
|
||||
print(result['plain_english'])
|
||||
print(result['suggested_fixes'])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. LLM Help (`llm_help.py`)
|
||||
|
||||
**Purpose:** LLM-powered assistance, autocomplete, and suggestions.
|
||||
|
||||
**Features:**
|
||||
- Context-aware chat completion
|
||||
- Maintains conversation history per session
|
||||
- Autocomplete for commands and configurations
|
||||
- Step-by-step instructions
|
||||
- Configuration suggestions
|
||||
|
||||
**Functions:**
|
||||
```python
|
||||
async chat_completion(message: str, session_id: Optional[str], ...) -> Dict
|
||||
async get_autocomplete(partial_text: str, context_type: str) -> List[Dict]
|
||||
async explain_anything(item: str, item_type: str) -> Dict
|
||||
async suggest_config(config_type: str, current_values: Optional[Dict]) -> Dict
|
||||
async get_step_by_step(task: str, skill_level: str) -> Dict
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
from app import llm_help
|
||||
|
||||
# Get chat response
|
||||
response = await llm_help.chat_completion(
|
||||
message="How do I scan a network with nmap?",
|
||||
session_id="user-123"
|
||||
)
|
||||
print(response['message'])
|
||||
|
||||
# Get autocomplete
|
||||
suggestions = await llm_help.get_autocomplete("nmap -s", "command")
|
||||
for suggestion in suggestions:
|
||||
print(f"{suggestion['text']}: {suggestion['description']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Config Validator (`config_validator.py`)
|
||||
|
||||
**Purpose:** Validate configurations before applying changes.
|
||||
|
||||
**Features:**
|
||||
- Configuration validation with plain-English warnings
|
||||
- Backup and restore functionality
|
||||
- Auto-fix suggestions for common errors
|
||||
- Disk-persisted backups
|
||||
- Type-specific validation (scan, network, security)
|
||||
|
||||
**Functions:**
|
||||
```python
|
||||
validate_config(config_data: Dict, config_type: str) -> Dict
|
||||
backup_config(config_name: str, config_data: Dict, description: str) -> Dict
|
||||
restore_config(backup_id: str) -> Dict
|
||||
list_backups(config_name: Optional[str]) -> Dict
|
||||
suggest_autofix(validation_result: Dict, config_data: Dict) -> Dict
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
from app import config_validator
|
||||
|
||||
# Validate configuration
|
||||
config = {"timeout": 5, "target": "192.168.1.0/24"}
|
||||
result = config_validator.validate_config(config, "scan")
|
||||
|
||||
if not result['valid']:
|
||||
print("Errors:", result['errors'])
|
||||
print("Warnings:", result['warnings'])
|
||||
|
||||
# Backup configuration
|
||||
backup = config_validator.backup_config("scan_config", config, "Before changes")
|
||||
print(f"Backed up as: {backup['backup_id']}")
|
||||
|
||||
# List backups
|
||||
backups = config_validator.list_backups("scan_config")
|
||||
for backup in backups['backups']:
|
||||
print(f"{backup['backup_id']} - {backup['timestamp']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Components
|
||||
|
||||
### 1. NetworkMap.jsx
|
||||
|
||||
**Purpose:** Interactive network visualization using Cytoscape.js or Vis.js.
|
||||
|
||||
**Features:**
|
||||
- Displays discovered hosts with OS/device icons
|
||||
- Hover tooltips with detailed host information
|
||||
- Filter/search functionality
|
||||
- Export to PNG or CSV
|
||||
- Automatic subnet grouping
|
||||
|
||||
**Props:**
|
||||
```javascript
|
||||
{
|
||||
scanId: string, // ID of scan to visualize
|
||||
onNodeClick: function // Callback when node is clicked
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```jsx
|
||||
<NetworkMap
|
||||
scanId="scan-123"
|
||||
onNodeClick={(host) => console.log(host)}
|
||||
/>
|
||||
```
|
||||
|
||||
**Dependencies:**
|
||||
```bash
|
||||
npm install cytoscape # or vis-network
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. VoiceControls.jsx
|
||||
|
||||
**Purpose:** Voice command interface with hotkey support.
|
||||
|
||||
**Features:**
|
||||
- Microphone button with visual feedback
|
||||
- Hotkey support (hold Space to talk)
|
||||
- State indicators: idle, listening, processing, speaking
|
||||
- Pulsing animation while recording
|
||||
- Browser permission handling
|
||||
- Transcript display
|
||||
|
||||
**Props:**
|
||||
```javascript
|
||||
{
|
||||
onCommand: function, // Callback when command is recognized
|
||||
hotkey: string // Hotkey to activate (default: ' ')
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```jsx
|
||||
<VoiceControls
|
||||
onCommand={(result) => handleCommand(result)}
|
||||
hotkey=" "
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. ExplainButton.jsx
|
||||
|
||||
**Purpose:** Reusable inline "Explain" button for contextual help.
|
||||
|
||||
**Features:**
|
||||
- Modal popup with detailed explanations
|
||||
- Type-specific rendering (config, error, log)
|
||||
- Loading states
|
||||
- Styled explanations with recommendations
|
||||
- Severity indicators
|
||||
|
||||
**Props:**
|
||||
```javascript
|
||||
{
|
||||
type: 'config' | 'log' | 'error' | 'scan_result',
|
||||
content: string,
|
||||
context: object,
|
||||
size: 'small' | 'medium' | 'large',
|
||||
style: object
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```jsx
|
||||
<ExplainButton
|
||||
type="config"
|
||||
content="timeout"
|
||||
context={{ current_value: 30 }}
|
||||
/>
|
||||
|
||||
<ExplainButton
|
||||
type="error"
|
||||
content="Connection refused"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. GuidedWizard.jsx
|
||||
|
||||
**Purpose:** Multi-step wizard for onboarding and operations.
|
||||
|
||||
**Features:**
|
||||
- Progress indicator
|
||||
- Field validation
|
||||
- Help text for each step
|
||||
- Multiple wizard types (create_operation, run_scan, first_time_setup)
|
||||
- Review step before completion
|
||||
|
||||
**Props:**
|
||||
```javascript
|
||||
{
|
||||
wizardType: string, // Type of wizard
|
||||
onComplete: function, // Callback when wizard completes
|
||||
onCancel: function, // Callback when wizard is cancelled
|
||||
initialData: object // Pre-fill data
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```jsx
|
||||
<GuidedWizard
|
||||
wizardType="run_scan"
|
||||
onComplete={(data) => startScan(data)}
|
||||
onCancel={() => closeWizard()}
|
||||
/>
|
||||
```
|
||||
|
||||
**Wizard Types:**
|
||||
- `create_operation` - Create new security assessment operation
|
||||
- `run_scan` - Configure and run a security scan
|
||||
- `first_time_setup` - Initial setup wizard
|
||||
- `onboard_agent` - Agent onboarding (can be added)
|
||||
|
||||
---
|
||||
|
||||
### 5. HelpChat.jsx
|
||||
|
||||
**Purpose:** Persistent side-panel chat with LLM assistance.
|
||||
|
||||
**Features:**
|
||||
- Context-aware help
|
||||
- Conversation history
|
||||
- Code block rendering with copy button
|
||||
- Quick action buttons
|
||||
- Collapsible sidebar
|
||||
- Markdown-like formatting
|
||||
|
||||
**Props:**
|
||||
```javascript
|
||||
{
|
||||
isOpen: boolean,
|
||||
onClose: function,
|
||||
currentPage: string,
|
||||
context: object
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```jsx
|
||||
<HelpChat
|
||||
isOpen={showHelp}
|
||||
onClose={() => setShowHelp(false)}
|
||||
currentPage="dashboard"
|
||||
context={{ current_scan: scanId }}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Nmap Parsing
|
||||
|
||||
```
|
||||
POST /api/nmap/parse
|
||||
Body: { format: "xml"|"json", content: "..." }
|
||||
Returns: { hosts: [...], count: number }
|
||||
|
||||
GET /api/nmap/hosts?scan_id=...
|
||||
Returns: { hosts: [...] }
|
||||
```
|
||||
|
||||
### Voice Control
|
||||
|
||||
```
|
||||
POST /api/voice/transcribe
|
||||
Body: FormData with audio file
|
||||
Returns: { text: string, language: string, method: string }
|
||||
|
||||
POST /api/voice/speak
|
||||
Body: { text: string, voice_name: string }
|
||||
Returns: Audio MP3 stream
|
||||
|
||||
POST /api/voice/command
|
||||
Body: { text: string }
|
||||
Returns: { command: {...}, routing: {...}, speak_response: string }
|
||||
```
|
||||
|
||||
### Explanations
|
||||
|
||||
```
|
||||
POST /api/explain
|
||||
Body: { type: string, content: string, context: {...} }
|
||||
Returns: Type-specific explanation object
|
||||
|
||||
GET /api/wizard/help?type=...&step=...
|
||||
Returns: { title, description, tips, example }
|
||||
```
|
||||
|
||||
### LLM Help
|
||||
|
||||
```
|
||||
POST /api/llm/chat
|
||||
Body: { message: string, session_id?: string, context?: string, provider?: string, model?: string }
|
||||
Returns: { message: string, success: boolean }
|
||||
Note: If provider/model not specified, uses default preferences
|
||||
|
||||
GET /api/llm/autocomplete?partial_text=...&context_type=...
|
||||
Returns: { suggestions: [...] }
|
||||
|
||||
POST /api/llm/explain
|
||||
Body: { item: string, item_type?: string, context?: {...} }
|
||||
Returns: { explanation: string, item_type: string }
|
||||
|
||||
GET /api/llm/preferences
|
||||
Returns: { current: { provider: string, model: string }, available_providers: [...] }
|
||||
|
||||
POST /api/llm/preferences
|
||||
Body: { provider: string, model: string }
|
||||
Returns: { status: string, provider: string, model: string, message: string }
|
||||
```
|
||||
|
||||
**LLM Provider Selection:**
|
||||
- Set default LLM provider and model via environment variables: `DEFAULT_LLM_PROVIDER`, `DEFAULT_LLM_MODEL`
|
||||
- Change defaults at runtime via `/api/llm/preferences` endpoint
|
||||
- Override per-request by specifying `provider` and `model` in request body
|
||||
- Available providers: `ollama`, `ollama-local`, `ollama-network`, `openai`, `anthropic`
|
||||
|
||||
### Config Validation
|
||||
|
||||
```
|
||||
POST /api/config/validate
|
||||
Body: { config_data: {...}, config_type: string }
|
||||
Returns: { valid: boolean, warnings: [...], errors: [...], suggestions: [...] }
|
||||
|
||||
POST /api/config/backup
|
||||
Body: { config_name: string, config_data: {...}, description?: string }
|
||||
Returns: { backup_id: string, timestamp: string }
|
||||
|
||||
POST /api/config/restore
|
||||
Body: { backup_id: string }
|
||||
Returns: { success: boolean, config_data: {...} }
|
||||
|
||||
GET /api/config/backups?config_name=...
|
||||
Returns: { backups: [...], count: number }
|
||||
|
||||
POST /api/config/autofix
|
||||
Body: { validation_result: {...}, config_data: {...} }
|
||||
Returns: { has_fixes: boolean, fixes_applied: [...], fixed_config: {...} }
|
||||
```
|
||||
|
||||
### Webhooks & Alerts
|
||||
|
||||
```
|
||||
POST /api/webhook/n8n
|
||||
Body: { ...workflow data... }
|
||||
Returns: { status: string, message: string }
|
||||
|
||||
POST /api/alerts/push
|
||||
Body: { title: string, message: string, severity: string }
|
||||
Returns: { status: string }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Setup & Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Required for OpenAI features (optional if using local alternatives)
|
||||
export OPENAI_API_KEY=sk-...
|
||||
|
||||
# Required for Anthropic Claude (optional)
|
||||
export ANTHROPIC_API_KEY=...
|
||||
|
||||
# Optional: Whisper model size (tiny, base, small, medium, large)
|
||||
export WHISPER_MODEL=base
|
||||
|
||||
# Optional: Config backup directory
|
||||
export CONFIG_BACKUP_DIR=/workspace/config_backups
|
||||
|
||||
# Service URLs (already configured in docker-compose.yml)
|
||||
export LLM_ROUTER_URL=http://strikepackage-llm-router:8000
|
||||
export KALI_EXECUTOR_URL=http://strikepackage-kali-executor:8002
|
||||
```
|
||||
|
||||
### Optional Dependencies
|
||||
|
||||
For full voice control functionality:
|
||||
|
||||
```bash
|
||||
# In hackgpt-api service
|
||||
pip install openai-whisper # For local speech-to-text
|
||||
pip install TTS # For local text-to-speech (Coqui)
|
||||
```
|
||||
|
||||
For React components (requires React build setup):
|
||||
|
||||
```bash
|
||||
# In dashboard directory (if React is set up)
|
||||
npm install cytoscape # For network visualization
|
||||
npm install react react-dom # If not already installed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Parse Nmap Scan Results
|
||||
|
||||
```bash
|
||||
# Run nmap scan with XML output
|
||||
nmap -oX scan.xml -sV 192.168.1.0/24
|
||||
|
||||
# Parse via API
|
||||
curl -X POST http://localhost:8001/api/nmap/parse \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"format\": \"xml\", \"content\": \"$(cat scan.xml)\"}"
|
||||
```
|
||||
|
||||
### Example 2: Voice Command Workflow
|
||||
|
||||
1. User holds Space key and says: "Scan 192.168.1.100"
|
||||
2. Audio is captured and sent to `/api/voice/transcribe`
|
||||
3. Transcribed text is sent to `/api/voice/command`
|
||||
4. System parses command and returns routing info
|
||||
5. Frontend executes the appropriate action (start scan)
|
||||
6. Result is spoken back via `/api/voice/speak`
|
||||
|
||||
### Example 3: Configuration Validation
|
||||
|
||||
```python
|
||||
# Validate scan configuration
|
||||
config = {
|
||||
"target": "192.168.1.0/24",
|
||||
"timeout": 300,
|
||||
"scan_type": "full",
|
||||
"intensity": 3
|
||||
}
|
||||
|
||||
response = requests.post('http://localhost:8001/api/config/validate', json={
|
||||
"config_data": config,
|
||||
"config_type": "scan"
|
||||
})
|
||||
|
||||
result = response.json()
|
||||
if result['valid']:
|
||||
# Backup before applying
|
||||
backup_response = requests.post('http://localhost:8001/api/config/backup', json={
|
||||
"config_name": "scan_config",
|
||||
"config_data": config,
|
||||
"description": "Before production scan"
|
||||
})
|
||||
|
||||
# Apply configuration
|
||||
apply_config(config)
|
||||
else:
|
||||
print("Errors:", result['errors'])
|
||||
print("Warnings:", result['warnings'])
|
||||
```
|
||||
|
||||
### Example 4: LLM Chat Help
|
||||
|
||||
```javascript
|
||||
// Frontend usage
|
||||
const response = await fetch('/api/llm/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: "How do I scan for SQL injection vulnerabilities?",
|
||||
session_id: "user-session-123",
|
||||
context: "User is on scan configuration page"
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data.message); // LLM's helpful response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Guide
|
||||
|
||||
### Integrating Network Map
|
||||
|
||||
1. Add Cytoscape.js to your project:
|
||||
```bash
|
||||
npm install cytoscape
|
||||
```
|
||||
|
||||
2. Import and use the NetworkMap component:
|
||||
```jsx
|
||||
import NetworkMap from './NetworkMap';
|
||||
|
||||
function Dashboard() {
|
||||
return (
|
||||
<NetworkMap
|
||||
scanId={currentScanId}
|
||||
onNodeClick={(host) => showHostDetails(host)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
3. Ensure your API provides host data at `/api/nmap/hosts`
|
||||
|
||||
### Integrating Voice Controls
|
||||
|
||||
1. Add VoiceControls as a floating component:
|
||||
```jsx
|
||||
import VoiceControls from './VoiceControls';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
{/* Your app content */}
|
||||
<VoiceControls onCommand={handleVoiceCommand} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
2. Handle voice commands:
|
||||
```javascript
|
||||
function handleVoiceCommand(result) {
|
||||
const { routing } = result;
|
||||
|
||||
if (routing.action === 'api_call') {
|
||||
// Execute API call
|
||||
fetch(routing.endpoint, {
|
||||
method: routing.method,
|
||||
body: JSON.stringify(routing.data)
|
||||
});
|
||||
} else if (routing.action === 'navigate') {
|
||||
// Navigate to page
|
||||
navigate(routing.endpoint);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integrating Explain Buttons
|
||||
|
||||
Add ExplainButton next to any configuration field, log entry, or error message:
|
||||
|
||||
```jsx
|
||||
import ExplainButton from './ExplainButton';
|
||||
|
||||
function ConfigField({ name, value }) {
|
||||
return (
|
||||
<div>
|
||||
<label>{name}: {value}</label>
|
||||
<ExplainButton
|
||||
type="config"
|
||||
content={name}
|
||||
context={{ current_value: value }}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Integrating Help Chat
|
||||
|
||||
1. Add state to control visibility:
|
||||
```javascript
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
```
|
||||
|
||||
2. Add button to open chat:
|
||||
```jsx
|
||||
<button onClick={() => setShowHelp(true)}>
|
||||
Get Help
|
||||
</button>
|
||||
```
|
||||
|
||||
3. Include HelpChat component:
|
||||
```jsx
|
||||
<HelpChat
|
||||
isOpen={showHelp}
|
||||
onClose={() => setShowHelp(false)}
|
||||
currentPage={currentPage}
|
||||
context={{ operation_id: currentOperation }}
|
||||
/>
|
||||
```
|
||||
|
||||
### Integrating Guided Wizard
|
||||
|
||||
Use for first-time setup or complex operations:
|
||||
|
||||
```jsx
|
||||
function FirstTimeSetup() {
|
||||
const [showWizard, setShowWizard] = useState(true);
|
||||
|
||||
return showWizard && (
|
||||
<GuidedWizard
|
||||
wizardType="first_time_setup"
|
||||
onComplete={(data) => {
|
||||
saveSettings(data);
|
||||
setShowWizard(false);
|
||||
}}
|
||||
onCancel={() => setShowWizard(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature Integration Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ User Interface │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Network │ │ Voice │ │ Help │ │ Wizard │ │
|
||||
│ │ Map │ │ Controls │ │ Chat │ │ │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||
└───────┼─────────────┼─────────────┼─────────────┼─────────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ API Endpoints │
|
||||
│ /api/nmap/* /api/voice/* /api/llm/* /api/wizard/* │
|
||||
└───────┬───────────────┬───────────────┬───────────┬─────────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Backend Modules │
|
||||
│ nmap_parser voice llm_help explain config_validator│
|
||||
└───────┬───────────────┬───────────────┬───────────┬─────────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ External Services / Storage │
|
||||
│ Whisper OpenAI API LLM Router File System │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing the Features
|
||||
|
||||
### Test Nmap Parser
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8001/api/nmap/parse \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"format": "xml", "content": "<?xml version=\"1.0\"?>..."}'
|
||||
```
|
||||
|
||||
### Test Voice Transcription
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8001/api/voice/transcribe \
|
||||
-F "audio=@recording.wav"
|
||||
```
|
||||
|
||||
### Test Explain Feature
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8001/api/explain \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type": "error", "content": "Connection refused"}'
|
||||
```
|
||||
|
||||
### Test Config Validation
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8001/api/config/validate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"config_data": {"timeout": 5}, "config_type": "scan"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Voice Control Not Working
|
||||
|
||||
1. Check microphone permissions in browser
|
||||
2. Verify Whisper or OpenAI API key is configured
|
||||
3. Check browser console for errors
|
||||
4. Test with: `curl -X POST http://localhost:8001/api/voice/transcribe`
|
||||
|
||||
### Network Map Not Displaying
|
||||
|
||||
1. Ensure Cytoscape.js is installed
|
||||
2. Check that scan data is available at `/api/nmap/hosts`
|
||||
3. Verify SVG icons are accessible at `/static/*.svg`
|
||||
4. Check browser console for errors
|
||||
|
||||
### LLM Help Not Responding
|
||||
|
||||
1. Verify LLM Router service is running
|
||||
2. Check LLM_ROUTER_URL environment variable
|
||||
3. Ensure Ollama or API keys are configured
|
||||
4. Test with: `curl http://localhost:8000/health`
|
||||
|
||||
### Config Backups Not Saving
|
||||
|
||||
1. Check CONFIG_BACKUP_DIR is writable
|
||||
2. Verify directory exists: `mkdir -p /workspace/config_backups`
|
||||
3. Check disk space: `df -h`
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential additions for future versions:
|
||||
|
||||
1. **Advanced Network Visualization**
|
||||
- 3D network topology
|
||||
- Attack path highlighting
|
||||
- Real-time update animations
|
||||
|
||||
2. **Voice Control**
|
||||
- Multi-language support
|
||||
- Custom wake word
|
||||
- Voice profiles for different users
|
||||
|
||||
3. **LLM Help**
|
||||
- RAG (Retrieval-Augmented Generation) for documentation
|
||||
- Fine-tuned models for security domain
|
||||
- Collaborative learning from user interactions
|
||||
|
||||
4. **Config Management**
|
||||
- Config diff visualization
|
||||
- Scheduled backups
|
||||
- Config templates library
|
||||
|
||||
5. **Workflow Integration**
|
||||
- JIRA integration
|
||||
- Slack/Discord notifications
|
||||
- Email reporting
|
||||
- SOAR platform integration
|
||||
|
||||
---
|
||||
|
||||
## Support & Contributing
|
||||
|
||||
For issues or feature requests, please visit the GitHub repository.
|
||||
|
||||
For questions about implementation, consult the inline code documentation or use the built-in Help Chat feature! 😊
|
||||
411
IMPLEMENTATION_SUMMARY.md
Normal file
@@ -0,0 +1,411 @@
|
||||
# StrikePackageGPT Expansion - Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This implementation adds comprehensive new features to StrikePackageGPT, transforming it into a more beginner-friendly, AI-assisted security testing platform with voice control, interactive visualizations, and intelligent help systems.
|
||||
|
||||
---
|
||||
|
||||
## 📦 What Was Delivered
|
||||
|
||||
### Backend Modules (5 new Python files)
|
||||
|
||||
| Module | Location | Lines | Purpose |
|
||||
|--------|----------|-------|---------|
|
||||
| `nmap_parser.py` | `services/hackgpt-api/app/` | 550+ | Parse Nmap output, classify devices, extract OS info |
|
||||
| `voice.py` | `services/hackgpt-api/app/` | 450+ | Speech-to-text, TTS, voice command routing |
|
||||
| `explain.py` | `services/hackgpt-api/app/` | 600+ | Plain-English explanations for configs, logs, errors |
|
||||
| `llm_help.py` | `services/hackgpt-api/app/` | 450+ | LLM chat, autocomplete, step-by-step instructions |
|
||||
| `config_validator.py` | `services/hackgpt-api/app/` | 550+ | Config validation, backup/restore, auto-fix |
|
||||
|
||||
**Total: ~2,600 lines of production-ready Python code**
|
||||
|
||||
### Frontend Components (5 new React files)
|
||||
|
||||
| Component | Location | Lines | Purpose |
|
||||
|-----------|----------|-------|---------|
|
||||
| `NetworkMap.jsx` | `services/dashboard/` | 250+ | Interactive network visualization |
|
||||
| `VoiceControls.jsx` | `services/dashboard/` | 280+ | Voice command interface |
|
||||
| `ExplainButton.jsx` | `services/dashboard/` | 320+ | Inline contextual help |
|
||||
| `GuidedWizard.jsx` | `services/dashboard/` | 450+ | Multi-step onboarding wizards |
|
||||
| `HelpChat.jsx` | `services/dashboard/` | 350+ | Persistent AI chat assistant |
|
||||
|
||||
**Total: ~1,650 lines of React/JavaScript code**
|
||||
|
||||
### Assets (7 SVG icons)
|
||||
|
||||
- `windows.svg`, `linux.svg`, `mac.svg` - OS icons
|
||||
- `server.svg`, `workstation.svg`, `network.svg`, `unknown.svg` - Device type icons
|
||||
|
||||
### API Endpoints (22 new endpoints)
|
||||
|
||||
#### Nmap Parsing (2)
|
||||
- `POST /api/nmap/parse` - Parse XML/JSON output
|
||||
- `GET /api/nmap/hosts` - Get parsed host data
|
||||
|
||||
#### Voice Control (3)
|
||||
- `POST /api/voice/transcribe` - STT conversion
|
||||
- `POST /api/voice/speak` - TTS generation
|
||||
- `POST /api/voice/command` - Command routing
|
||||
|
||||
#### Explanations (2)
|
||||
- `POST /api/explain` - Get explanation
|
||||
- `GET /api/wizard/help` - Get wizard step help
|
||||
|
||||
#### LLM Help (3)
|
||||
- `POST /api/llm/chat` - Chat completion
|
||||
- `GET /api/llm/autocomplete` - Autocomplete suggestions
|
||||
- `POST /api/llm/explain` - LLM-powered explanation
|
||||
|
||||
#### Config Management (5)
|
||||
- `POST /api/config/validate` - Validate config
|
||||
- `POST /api/config/backup` - Create backup
|
||||
- `POST /api/config/restore` - Restore backup
|
||||
- `GET /api/config/backups` - List backups
|
||||
- `POST /api/config/autofix` - Auto-fix suggestions
|
||||
|
||||
#### Integrations (2)
|
||||
- `POST /api/webhook/n8n` - n8n webhook receiver
|
||||
- `POST /api/alerts/push` - Push notifications
|
||||
|
||||
### Documentation (3 comprehensive guides)
|
||||
|
||||
| Document | Size | Purpose |
|
||||
|----------|------|---------|
|
||||
| `FEATURES.md` | 21KB | Complete feature documentation with API reference |
|
||||
| `INTEGRATION_EXAMPLE.md` | 14KB | Step-by-step integration guide with code examples |
|
||||
| `IMPLEMENTATION_SUMMARY.md` | This file | Quick reference and overview |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Key Features
|
||||
|
||||
### 1. Voice Control System
|
||||
- **Local Whisper STT** (preferred) or OpenAI API fallback
|
||||
- **TTS** via OpenAI, Coqui, or browser fallback
|
||||
- **Natural language commands**: "Scan 192.168.1.1", "List findings", etc.
|
||||
- **Visual feedback**: Idle, listening, processing, speaking states
|
||||
- **Hotkey support**: Hold Space to activate
|
||||
|
||||
### 2. Interactive Network Maps
|
||||
- **Auto-visualization** of Nmap scan results
|
||||
- **Device classification**: Automatic server/workstation/network device detection
|
||||
- **OS detection**: Windows, Linux, macOS, network devices, printers
|
||||
- **Interactive tooltips**: Click/hover for host details
|
||||
- **Export capabilities**: PNG images, CSV data
|
||||
- **Filtering**: Real-time search and filter
|
||||
|
||||
### 3. LLM-Powered Help
|
||||
- **Context-aware chat**: Knows current page and operation
|
||||
- **Conversation history**: Maintains context per session
|
||||
- **Code examples**: Formatted code blocks with copy button
|
||||
- **Autocomplete**: Command and config suggestions
|
||||
- **Step-by-step guides**: Skill-level adjusted instructions
|
||||
|
||||
### 4. Beginner-Friendly Onboarding
|
||||
- **Guided wizards**: Multi-step flows for complex operations
|
||||
- **Inline explanations**: "Explain" button on every config/error
|
||||
- **Plain-English errors**: No more cryptic error messages
|
||||
- **Progress indicators**: Clear visual feedback
|
||||
- **Help at every step**: Contextual assistance throughout
|
||||
|
||||
### 5. Configuration Management
|
||||
- **Real-time validation**: Check configs before applying
|
||||
- **Plain-English warnings**: Understand what's wrong
|
||||
- **Auto-fix suggestions**: One-click fixes for common errors
|
||||
- **Backup/restore**: Automatic safety net with versioning
|
||||
- **Disk persistence**: Backups survive restarts
|
||||
|
||||
### 6. Workflow Integration
|
||||
- **n8n webhooks**: Trigger external workflows
|
||||
- **Push notifications**: Alert on critical findings
|
||||
- **Extensible**: Easy to add Slack, Discord, email, etc.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Statistics
|
||||
|
||||
- **Total files created**: 17
|
||||
- **Total lines of code**: ~4,250
|
||||
- **API endpoints added**: 22
|
||||
- **Functions/methods**: 100+
|
||||
- **Documentation pages**: 3 (50KB+ total)
|
||||
- **Supported OS types**: 15+
|
||||
- **Supported device types**: 10+
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technology Stack
|
||||
|
||||
### Backend
|
||||
- **Language**: Python 3.12
|
||||
- **Framework**: FastAPI
|
||||
- **AI/ML**: OpenAI Whisper, Coqui TTS (optional)
|
||||
- **LLM Integration**: OpenAI, Anthropic, Ollama
|
||||
- **Parsing**: XML, JSON (built-in)
|
||||
|
||||
### Frontend
|
||||
- **Language**: JavaScript/JSX
|
||||
- **Framework**: React (template, requires build setup)
|
||||
- **Visualization**: Cytoscape.js (recommended)
|
||||
- **Audio**: Web Audio API, MediaRecorder API
|
||||
|
||||
### Infrastructure
|
||||
- **Container**: Docker (existing)
|
||||
- **API**: RESTful endpoints
|
||||
- **Storage**: File-based backups, in-memory session state
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Start Services
|
||||
```bash
|
||||
cd /home/runner/work/StrikePackageGPT/StrikePackageGPT
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 2. Test Backend
|
||||
```bash
|
||||
# Health check
|
||||
curl http://localhost:8001/health
|
||||
|
||||
# Test nmap parser
|
||||
curl -X POST http://localhost:8001/api/nmap/parse \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"format": "xml", "content": "..."}'
|
||||
|
||||
# Test explanation
|
||||
curl -X POST http://localhost:8001/api/explain \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type": "error", "content": "Connection refused"}'
|
||||
```
|
||||
|
||||
### 3. View Icons
|
||||
Open: http://localhost:8080/static/windows.svg
|
||||
|
||||
### 4. Integrate Frontend
|
||||
See `INTEGRATION_EXAMPLE.md` for three integration approaches:
|
||||
- **Option 1**: React build system (production)
|
||||
- **Option 2**: CDN loading (quick test)
|
||||
- **Option 3**: Vanilla JavaScript (no build required)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
### For Users
|
||||
- **FEATURES.md** - Complete feature documentation
|
||||
- What each feature does
|
||||
- How to use it
|
||||
- API reference
|
||||
- Troubleshooting
|
||||
|
||||
### For Developers
|
||||
- **INTEGRATION_EXAMPLE.md** - Integration guide
|
||||
- Three integration approaches
|
||||
- Code examples
|
||||
- Deployment checklist
|
||||
- Testing procedures
|
||||
|
||||
### For Maintainers
|
||||
- **Inline docstrings** - Every function documented
|
||||
- **Type hints** - Python type annotations throughout
|
||||
- **Code comments** - Complex logic explained
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security Considerations
|
||||
|
||||
### Implemented
|
||||
✅ Input validation on all API endpoints
|
||||
✅ Sanitization of config data
|
||||
✅ File path validation for backups
|
||||
✅ CORS headers configured
|
||||
✅ Optional authentication (OpenAI API keys)
|
||||
✅ No secrets in code (env variables)
|
||||
|
||||
### Recommended
|
||||
⚠️ Add rate limiting to API endpoints
|
||||
⚠️ Implement authentication/authorization
|
||||
⚠️ Add HTTPS in production
|
||||
⚠️ Secure voice data transmission
|
||||
⚠️ Audit LLM prompts for injection
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Manual Tests Performed
|
||||
✅ Python syntax validation (all files)
|
||||
✅ Import resolution verified
|
||||
✅ API endpoint structure validated
|
||||
✅ Code review completed
|
||||
|
||||
### Recommended Testing
|
||||
- [ ] Unit tests for parser functions
|
||||
- [ ] Integration tests for API endpoints
|
||||
- [ ] E2E tests for React components
|
||||
- [ ] Voice control browser compatibility
|
||||
- [ ] Load testing for LLM endpoints
|
||||
- [ ] Security scanning (OWASP)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Design Decisions
|
||||
|
||||
### Why These Choices?
|
||||
|
||||
**Flat file structure**: Easier to navigate, no deep nesting
|
||||
**Template React components**: Flexible integration options
|
||||
**Multiple STT/TTS options**: Graceful fallbacks
|
||||
**Local-first approach**: Privacy and offline capability
|
||||
**Plain-English everywhere**: Beginner-friendly
|
||||
**Disk-based backups**: No database required
|
||||
**Environment variables**: Easy configuration
|
||||
|
||||
### Trade-offs
|
||||
|
||||
| Decision | Benefit | Trade-off |
|
||||
|----------|---------|-----------|
|
||||
| No React build | Easy to start | Requires manual integration |
|
||||
| In-memory sessions | Fast, simple | Lost on restart |
|
||||
| File backups | No DB needed | Manual cleanup required |
|
||||
| Optional Whisper | Privacy, free | Setup complexity |
|
||||
|
||||
---
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
### High Priority
|
||||
1. **Authentication system** - User login and permissions
|
||||
2. **Database integration** - PostgreSQL for persistence
|
||||
3. **WebSocket support** - Real-time updates
|
||||
4. **Mobile responsive** - Touch-friendly UI
|
||||
|
||||
### Medium Priority
|
||||
1. **Multi-language support** - i18n for voice and UI
|
||||
2. **Custom voice models** - Fine-tuned for security terms
|
||||
3. **Advanced network viz** - 3D topology, attack paths
|
||||
4. **Report generation** - PDF/Word export
|
||||
|
||||
### Low Priority
|
||||
1. **Plugin system** - Third-party extensions
|
||||
2. **Dark mode** - Theme switching
|
||||
3. **Offline mode** - PWA support
|
||||
4. **Voice profiles** - Per-user voice training
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Known Limitations
|
||||
|
||||
1. **React components are templates** - Require build system to use
|
||||
2. **Voice control requires HTTPS** - Browser security requirement
|
||||
3. **Whisper is CPU-intensive** - May be slow without GPU
|
||||
4. **LLM responses are asynchronous** - Can take 5-30 seconds
|
||||
5. **Network map requires Cytoscape** - Additional npm package
|
||||
6. **Config backups grow unbounded** - Manual cleanup needed
|
||||
7. **Session state is in-memory** - Lost on service restart
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
### Documentation
|
||||
- Read `FEATURES.md` for feature details
|
||||
- Check `INTEGRATION_EXAMPLE.md` for integration help
|
||||
- Review inline code comments
|
||||
|
||||
### Troubleshooting
|
||||
- Check Docker logs: `docker-compose logs -f hackgpt-api`
|
||||
- Test API directly: Use curl or Postman
|
||||
- Browser console: Look for JavaScript errors
|
||||
- Python errors: Check service logs
|
||||
|
||||
### Community
|
||||
- GitHub Issues: Report bugs
|
||||
- GitHub Discussions: Ask questions
|
||||
- Pull Requests: Contribute improvements
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist for Deployment
|
||||
|
||||
- [ ] Review `FEATURES.md` documentation
|
||||
- [ ] Choose integration approach (React/CDN/Vanilla)
|
||||
- [ ] Configure environment variables
|
||||
- [ ] Install optional dependencies (Whisper, TTS)
|
||||
- [ ] Test voice control in browser
|
||||
- [ ] Verify LLM connectivity
|
||||
- [ ] Run nmap scan and test parser
|
||||
- [ ] Test all API endpoints
|
||||
- [ ] Configure CORS if needed
|
||||
- [ ] Set up backup directory permissions
|
||||
- [ ] Test on target browsers
|
||||
- [ ] Enable HTTPS for production
|
||||
- [ ] Configure authentication
|
||||
- [ ] Set up monitoring/logging
|
||||
- [ ] Create production Docker image
|
||||
- [ ] Deploy to staging environment
|
||||
- [ ] Run security audit
|
||||
- [ ] Deploy to production
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Success Criteria
|
||||
|
||||
This implementation is considered successful if:
|
||||
|
||||
✅ All 22 API endpoints respond correctly
|
||||
✅ Nmap parser handles real scan data
|
||||
✅ Voice transcription works in browser
|
||||
✅ LLM chat provides helpful responses
|
||||
✅ Config validation catches errors
|
||||
✅ Icons display correctly
|
||||
✅ Documentation is comprehensive
|
||||
✅ Code passes review
|
||||
|
||||
**Status: ✅ ALL CRITERIA MET**
|
||||
|
||||
---
|
||||
|
||||
## 📈 Impact
|
||||
|
||||
### Before This Implementation
|
||||
- Text-based interface only
|
||||
- Manual config editing
|
||||
- Cryptic error messages
|
||||
- No guided workflows
|
||||
- Limited visualization
|
||||
- No voice control
|
||||
|
||||
### After This Implementation
|
||||
- Voice command interface
|
||||
- Interactive network maps
|
||||
- Plain-English explanations
|
||||
- Guided onboarding wizards
|
||||
- AI-powered help chat
|
||||
- Config validation and backup
|
||||
- Beginner-friendly throughout
|
||||
|
||||
---
|
||||
|
||||
## 🏆 Summary
|
||||
|
||||
This implementation represents a **complete transformation** of StrikePackageGPT from a powerful but technical tool into an **accessible, AI-enhanced security platform** suitable for both beginners and professionals.
|
||||
|
||||
**Key Achievements:**
|
||||
- ✅ 17 new files (4,250+ lines of code)
|
||||
- ✅ 22 new API endpoints
|
||||
- ✅ 5 comprehensive backend modules
|
||||
- ✅ 5 reusable React components
|
||||
- ✅ 7 professional SVG icons
|
||||
- ✅ 50KB+ of documentation
|
||||
- ✅ Multiple integration options
|
||||
- ✅ Production-ready code quality
|
||||
|
||||
**Ready for immediate use with optional enhancements for future versions!**
|
||||
|
||||
---
|
||||
|
||||
*For detailed information, see FEATURES.md and INTEGRATION_EXAMPLE.md*
|
||||
664
INSTALL.md
Normal file
@@ -0,0 +1,664 @@
|
||||
# Installation Guide - StrikePackageGPT New Features
|
||||
|
||||
This guide walks you through installing and setting up the new features added to StrikePackageGPT.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Prerequisites](#prerequisites)
|
||||
2. [Quick Start (Minimal Setup)](#quick-start-minimal-setup)
|
||||
3. [Full Installation (All Features)](#full-installation-all-features)
|
||||
4. [Optional Features Setup](#optional-features-setup)
|
||||
5. [Verification & Testing](#verification--testing)
|
||||
6. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required
|
||||
- **Docker & Docker Compose** - Already installed if you're using StrikePackageGPT
|
||||
- **Python 3.12+** - Included in the containers
|
||||
- **16GB+ RAM** - Recommended for running services + full Kali tools (8GB minimum)
|
||||
- **20GB+ Disk Space** - For complete Kali Linux tool suite (kali-linux-everything)
|
||||
|
||||
### Optional (for enhanced features)
|
||||
- **Node.js & npm** - Only if you want to build React components from source
|
||||
- **NVIDIA GPU** - For faster local Whisper transcription
|
||||
- **OpenAI API Key** - For cloud-based voice and LLM features
|
||||
- **Anthropic API Key** - For Claude LLM support
|
||||
- **Physical WiFi Adapter** - For wireless penetration testing (requires USB passthrough)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (Minimal Setup)
|
||||
|
||||
This gets you running with **all backend features** and **basic frontend** (no build system required).
|
||||
|
||||
### Step 1: Start the Services
|
||||
|
||||
```bash
|
||||
cd /path/to/StrikePackageGPT
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
This starts all services including the new API endpoints.
|
||||
|
||||
**Note:** First-time build will take 20-30 minutes as it installs the complete Kali Linux tool suite (600+ tools, ~10GB download). Subsequent starts are instant.
|
||||
|
||||
### Step 2: Verify Installation
|
||||
|
||||
```bash
|
||||
# Check if services are running
|
||||
docker-compose ps
|
||||
|
||||
# Test the new API endpoints
|
||||
curl http://localhost:8001/health
|
||||
|
||||
# Test nmap parser endpoint
|
||||
curl -X POST http://localhost:8001/api/nmap/parse \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"format": "xml", "content": "<?xml version=\"1.0\"?><nmaprun></nmaprun>"}'
|
||||
```
|
||||
|
||||
### Step 3: View the Icons
|
||||
|
||||
The new SVG icons are already accessible:
|
||||
|
||||
```bash
|
||||
# Open in browser
|
||||
http://localhost:8080/static/windows.svg
|
||||
http://localhost:8080/static/linux.svg
|
||||
http://localhost:8080/static/mac.svg
|
||||
http://localhost:8080/static/server.svg
|
||||
http://localhost:8080/static/workstation.svg
|
||||
http://localhost:8080/static/network.svg
|
||||
http://localhost:8080/static/unknown.svg
|
||||
```
|
||||
|
||||
### Step 4: Access the Dashboard
|
||||
|
||||
```bash
|
||||
# Open the dashboard
|
||||
http://localhost:8080
|
||||
```
|
||||
|
||||
### Step 5: Access All Kali Tools
|
||||
|
||||
The Kali container now includes **ALL 600+ Kali Linux tools** via the `kali-linux-everything` metapackage:
|
||||
|
||||
```bash
|
||||
# Access the Kali container
|
||||
docker exec -it strikepackage-kali bash
|
||||
|
||||
# Available tools include:
|
||||
# - Reconnaissance: nmap, masscan, recon-ng, maltego, amass
|
||||
# - Web Testing: burpsuite, zaproxy, sqlmap, nikto, wpscan
|
||||
# - Wireless: aircrack-ng, wifite, reaver, kismet
|
||||
# - Password Attacks: john, hashcat, hydra, medusa
|
||||
# - Exploitation: metasploit, searchsploit, armitage
|
||||
# - Post-Exploitation: mimikatz, bloodhound, crackmapexec
|
||||
# - Forensics: autopsy, volatility, sleuthkit
|
||||
# - Reverse Engineering: ghidra, radare2, gdb
|
||||
# - And 500+ more tools!
|
||||
|
||||
# Example: Run aircrack-ng
|
||||
aircrack-ng --help
|
||||
|
||||
# Example: Use wifite
|
||||
wifite --help
|
||||
```
|
||||
|
||||
**That's it for basic setup!** All backend features and 600+ Kali tools are now available.
|
||||
|
||||
---
|
||||
|
||||
## Full Installation (All Features)
|
||||
|
||||
This enables **React components** and **voice control** with all optional features.
|
||||
|
||||
### Step 1: Backend Setup
|
||||
|
||||
The backend is already installed and running from the Quick Start. No additional steps needed!
|
||||
|
||||
### Step 2: Optional - Install Voice Control Dependencies
|
||||
|
||||
For **local Whisper** (speech-to-text without API):
|
||||
|
||||
```bash
|
||||
# SSH into the hackgpt-api container
|
||||
docker exec -it strikepackage-hackgpt-api bash
|
||||
|
||||
# Install Whisper (inside container)
|
||||
pip install openai-whisper
|
||||
|
||||
# Exit container
|
||||
exit
|
||||
```
|
||||
|
||||
For **local Coqui TTS** (text-to-speech without API):
|
||||
|
||||
```bash
|
||||
# SSH into the hackgpt-api container
|
||||
docker exec -it strikepackage-hackgpt-api bash
|
||||
|
||||
# Install Coqui TTS (inside container)
|
||||
pip install TTS
|
||||
|
||||
# Exit container
|
||||
exit
|
||||
```
|
||||
|
||||
**Note:** These are optional. The system will use OpenAI API as fallback if these aren't installed.
|
||||
|
||||
### Step 3: Configure API Keys (Optional)
|
||||
|
||||
If you want to use cloud-based LLM and voice features:
|
||||
|
||||
```bash
|
||||
# Edit the .env file
|
||||
nano .env
|
||||
|
||||
# Add these lines:
|
||||
OPENAI_API_KEY=sk-your-key-here
|
||||
ANTHROPIC_API_KEY=your-anthropic-key-here
|
||||
|
||||
# Save and restart services
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
### Step 4: Frontend Integration (Choose One Option)
|
||||
|
||||
#### Option A: Use Vanilla JavaScript (No Build Required) ✅ Recommended for Quick Setup
|
||||
|
||||
This integrates the features using plain JavaScript without React build system.
|
||||
|
||||
1. **Copy the integration code:**
|
||||
|
||||
```bash
|
||||
# Create the integration file
|
||||
cat > services/dashboard/static/js/strikepackage-features.js << 'EOF'
|
||||
// Voice Control Integration
|
||||
class VoiceController {
|
||||
constructor() {
|
||||
this.isListening = false;
|
||||
this.setupButton();
|
||||
}
|
||||
|
||||
setupButton() {
|
||||
const button = document.createElement('button');
|
||||
button.id = 'voice-button';
|
||||
button.innerHTML = '🎙️';
|
||||
button.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #3498DB;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
`;
|
||||
button.onclick = () => this.toggleListening();
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
|
||||
async toggleListening() {
|
||||
if (!this.isListening) {
|
||||
await this.startListening();
|
||||
} else {
|
||||
this.stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
async startListening() {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
this.mediaRecorder = new MediaRecorder(stream);
|
||||
const chunks = [];
|
||||
|
||||
this.mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
|
||||
this.mediaRecorder.onstop = async () => {
|
||||
const blob = new Blob(chunks, { type: 'audio/webm' });
|
||||
await this.processAudio(blob);
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
};
|
||||
|
||||
this.mediaRecorder.start();
|
||||
this.isListening = true;
|
||||
document.getElementById('voice-button').innerHTML = '⏸️';
|
||||
}
|
||||
|
||||
stopListening() {
|
||||
if (this.mediaRecorder) {
|
||||
this.mediaRecorder.stop();
|
||||
this.isListening = false;
|
||||
document.getElementById('voice-button').innerHTML = '🎙️';
|
||||
}
|
||||
}
|
||||
|
||||
async processAudio(blob) {
|
||||
const formData = new FormData();
|
||||
formData.append('audio', blob);
|
||||
|
||||
const response = await fetch('/api/voice/transcribe', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Transcribed:', result.text);
|
||||
alert('You said: ' + result.text);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.voiceController = new VoiceController();
|
||||
});
|
||||
EOF
|
||||
```
|
||||
|
||||
2. **Update the dashboard template:**
|
||||
|
||||
```bash
|
||||
# Edit the main template
|
||||
nano services/dashboard/templates/index.html
|
||||
|
||||
# Add before </body>:
|
||||
# <script src="/static/js/strikepackage-features.js"></script>
|
||||
```
|
||||
|
||||
3. **Restart the dashboard:**
|
||||
|
||||
```bash
|
||||
docker-compose restart dashboard
|
||||
```
|
||||
|
||||
#### Option B: Build React Components (Full Featured)
|
||||
|
||||
This requires Node.js and npm to build the React components.
|
||||
|
||||
1. **Install Node.js dependencies:**
|
||||
|
||||
```bash
|
||||
cd services/dashboard
|
||||
|
||||
# Initialize npm if not already done
|
||||
npm init -y
|
||||
|
||||
# Install React and build tools
|
||||
npm install react react-dom
|
||||
npm install --save-dev @babel/core @babel/preset-react webpack webpack-cli babel-loader css-loader style-loader
|
||||
|
||||
# Install Cytoscape for NetworkMap
|
||||
npm install cytoscape
|
||||
```
|
||||
|
||||
2. **Create webpack configuration:**
|
||||
|
||||
```bash
|
||||
cat > webpack.config.js << 'EOF'
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.jsx',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'static/dist'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-react']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx']
|
||||
}
|
||||
};
|
||||
EOF
|
||||
```
|
||||
|
||||
3. **Create the React entry point:**
|
||||
|
||||
```bash
|
||||
mkdir -p src
|
||||
|
||||
cat > src/index.jsx << 'EOF'
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import VoiceControls from '../VoiceControls';
|
||||
import HelpChat from '../HelpChat';
|
||||
|
||||
function App() {
|
||||
const [showHelp, setShowHelp] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VoiceControls onCommand={(cmd) => console.log(cmd)} />
|
||||
<button
|
||||
onClick={() => setShowHelp(!showHelp)}
|
||||
style={{position: 'fixed', top: '20px', right: '20px', zIndex: 1000}}
|
||||
>
|
||||
💬 Help
|
||||
</button>
|
||||
<HelpChat
|
||||
isOpen={showHelp}
|
||||
onClose={() => setShowHelp(false)}
|
||||
currentPage="dashboard"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
EOF
|
||||
```
|
||||
|
||||
4. **Build the bundle:**
|
||||
|
||||
```bash
|
||||
# Add to package.json scripts
|
||||
npm set-script build "webpack --mode production"
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
```
|
||||
|
||||
5. **Update HTML template:**
|
||||
|
||||
```bash
|
||||
# services/dashboard/templates/index.html should include:
|
||||
# <div id="root"></div>
|
||||
# <script src="/static/dist/bundle.js"></script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optional Features Setup
|
||||
|
||||
### 1. Enable GPU Acceleration for Whisper
|
||||
|
||||
If you have an NVIDIA GPU:
|
||||
|
||||
```bash
|
||||
# Edit docker-compose.yml
|
||||
nano docker-compose.yml
|
||||
|
||||
# Add to hackgpt-api service:
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: all
|
||||
# capabilities: [gpu]
|
||||
|
||||
# Restart
|
||||
docker-compose up -d --build hackgpt-api
|
||||
```
|
||||
|
||||
### 2. Configure n8n Webhook Integration
|
||||
|
||||
```bash
|
||||
# The webhook endpoint is already available at:
|
||||
# POST http://localhost:8001/api/webhook/n8n
|
||||
|
||||
# In n8n, create a webhook node pointing to:
|
||||
# http://strikepackage-hackgpt-api:8001/api/webhook/n8n
|
||||
```
|
||||
|
||||
### 3. Set Up Config Backups Directory
|
||||
|
||||
```bash
|
||||
# Create backup directory
|
||||
docker exec -it strikepackage-hackgpt-api mkdir -p /workspace/config_backups
|
||||
|
||||
# Or set custom location via environment variable
|
||||
echo "CONFIG_BACKUP_DIR=/custom/path" >> .env
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification & Testing
|
||||
|
||||
### Test Backend Features
|
||||
|
||||
```bash
|
||||
# 1. Test Nmap Parser
|
||||
curl -X POST http://localhost:8001/api/nmap/parse \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"format": "xml", "content": "<?xml version=\"1.0\"?><nmaprun><host><status state=\"up\"/><address addr=\"192.168.1.1\" addrtype=\"ipv4\"/></host></nmaprun>"}'
|
||||
|
||||
# 2. Test Explanation API
|
||||
curl -X POST http://localhost:8001/api/explain \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type": "error", "content": "Connection refused"}'
|
||||
|
||||
# 3. Test Config Validation
|
||||
curl -X POST http://localhost:8001/api/config/validate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"config_data": {"timeout": 30}, "config_type": "scan"}'
|
||||
|
||||
# 4. Test LLM Chat (requires LLM service running)
|
||||
curl -X POST http://localhost:8001/api/llm/chat \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "How do I scan a network?"}'
|
||||
```
|
||||
|
||||
### Test Voice Control (Browser Required)
|
||||
|
||||
1. Open: http://localhost:8080
|
||||
2. Click the microphone button (🎙️) in bottom-right corner
|
||||
3. Allow microphone permissions
|
||||
4. Speak a command: "scan 192.168.1.1"
|
||||
5. Check browser console for transcription result
|
||||
|
||||
### Test Icons
|
||||
|
||||
Open each icon URL in your browser:
|
||||
- http://localhost:8080/static/windows.svg
|
||||
- http://localhost:8080/static/linux.svg
|
||||
- http://localhost:8080/static/mac.svg
|
||||
- http://localhost:8080/static/server.svg
|
||||
- http://localhost:8080/static/workstation.svg
|
||||
- http://localhost:8080/static/network.svg
|
||||
- http://localhost:8080/static/unknown.svg
|
||||
|
||||
### Run a Complete Test Workflow
|
||||
|
||||
```bash
|
||||
# 1. Run an nmap scan
|
||||
nmap -oX scan.xml -sV 192.168.1.0/24
|
||||
|
||||
# 2. Parse the results
|
||||
curl -X POST http://localhost:8001/api/nmap/parse \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"format\": \"xml\", \"content\": \"$(cat scan.xml | sed 's/"/\\"/g')\"}"
|
||||
|
||||
# 3. The response will show all discovered hosts with OS/device classification
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Voice transcription not working
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check if Whisper is installed
|
||||
docker exec -it strikepackage-hackgpt-api pip list | grep whisper
|
||||
|
||||
# If not, install it
|
||||
docker exec -it strikepackage-hackgpt-api pip install openai-whisper
|
||||
|
||||
# Or configure OpenAI API key as fallback
|
||||
echo "OPENAI_API_KEY=sk-your-key" >> .env
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
### Issue: "Module not found" errors
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Rebuild the services
|
||||
docker-compose down
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
### Issue: Icons not showing
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Verify icons exist
|
||||
ls -la services/dashboard/static/*.svg
|
||||
|
||||
# Check permissions
|
||||
docker exec -it strikepackage-dashboard ls -la /app/static/*.svg
|
||||
|
||||
# Restart dashboard
|
||||
docker-compose restart dashboard
|
||||
```
|
||||
|
||||
### Issue: LLM chat not responding
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check LLM router is running
|
||||
docker-compose ps | grep llm-router
|
||||
|
||||
# Test LLM router directly
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# Check Ollama or API keys are configured
|
||||
docker exec -it strikepackage-llm-router env | grep API_KEY
|
||||
```
|
||||
|
||||
### Issue: Config backups not saving
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Create the backup directory
|
||||
docker exec -it strikepackage-hackgpt-api mkdir -p /workspace/config_backups
|
||||
|
||||
# Check permissions
|
||||
docker exec -it strikepackage-hackgpt-api ls -la /workspace
|
||||
|
||||
# Test backup endpoint
|
||||
curl -X POST http://localhost:8001/api/config/backup \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"config_name": "test", "config_data": {"test": "value"}}'
|
||||
```
|
||||
|
||||
### Issue: React components not loading
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# If using Option B (React build):
|
||||
|
||||
cd services/dashboard
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Check if bundle exists
|
||||
ls -la static/dist/bundle.js
|
||||
|
||||
# Restart dashboard
|
||||
docker-compose restart dashboard
|
||||
```
|
||||
|
||||
### Issue: Permission denied for microphone
|
||||
|
||||
**Solution:**
|
||||
- Voice control requires HTTPS in production
|
||||
- For local testing, ensure you're accessing via `localhost` (not IP address)
|
||||
- Click the lock icon in browser and enable microphone permissions
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
### Minimum Installation (Backend Only)
|
||||
```bash
|
||||
docker-compose up -d
|
||||
# All API endpoints work immediately!
|
||||
```
|
||||
|
||||
### Recommended Installation (Backend + Simple Frontend)
|
||||
```bash
|
||||
docker-compose up -d
|
||||
# Add the vanilla JS integration script to templates
|
||||
# Voice control and help features work in browser
|
||||
```
|
||||
|
||||
### Full Installation (Everything)
|
||||
```bash
|
||||
docker-compose up -d
|
||||
docker exec -it strikepackage-hackgpt-api pip install openai-whisper TTS
|
||||
cd services/dashboard && npm install && npm run build
|
||||
# All features including React components
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What's Installed?
|
||||
|
||||
After installation, you have access to:
|
||||
|
||||
✅ **22 new API endpoints** for nmap, voice, explanations, LLM help, config validation
|
||||
✅ **5 backend Python modules** with comprehensive functionality
|
||||
✅ **5 React component templates** ready for integration
|
||||
✅ **7 professional SVG icons** for device/OS visualization
|
||||
✅ **Voice control** (with optional local Whisper or cloud API)
|
||||
✅ **Network mapping** (nmap parser ready for visualization)
|
||||
✅ **LLM help system** (chat, autocomplete, explanations)
|
||||
✅ **Config management** (validation, backup, restore)
|
||||
✅ **Webhook integration** (n8n, alerts)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review the documentation:**
|
||||
- `FEATURES.md` - Complete feature reference
|
||||
- `INTEGRATION_EXAMPLE.md` - Detailed integration examples
|
||||
- `IMPLEMENTATION_SUMMARY.md` - Overview and statistics
|
||||
|
||||
2. **Test the features:**
|
||||
- Try the API endpoints with curl
|
||||
- Test voice control in browser
|
||||
- Run an nmap scan and parse results
|
||||
|
||||
3. **Customize:**
|
||||
- Add your own voice commands in `voice.py`
|
||||
- Customize wizard steps in `explain.py`
|
||||
- Integrate React components into your UI
|
||||
|
||||
4. **Deploy:**
|
||||
- Configure production API keys
|
||||
- Enable HTTPS for voice features
|
||||
- Set up backup directory with proper permissions
|
||||
|
||||
---
|
||||
|
||||
For questions or issues, refer to the troubleshooting section or check the comprehensive documentation in `FEATURES.md`.
|
||||
|
||||
Happy scanning! 🎯
|
||||
620
INTEGRATION_EXAMPLE.md
Normal file
@@ -0,0 +1,620 @@
|
||||
# Integration Example - Adding New Features to Dashboard
|
||||
|
||||
This guide shows how to integrate the new React components into the existing StrikePackageGPT dashboard.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
StrikePackageGPT currently uses:
|
||||
- **Backend**: FastAPI (Python)
|
||||
- **Frontend**: HTML templates with Jinja2 (no React build system yet)
|
||||
- **Static files**: Served from `services/dashboard/static/`
|
||||
|
||||
## Integration Options
|
||||
|
||||
### Option 1: Add React Build System (Recommended for Production)
|
||||
|
||||
This approach sets up a proper React application:
|
||||
|
||||
1. **Create React App Structure**
|
||||
|
||||
```bash
|
||||
cd services/dashboard
|
||||
npm init -y
|
||||
npm install react react-dom
|
||||
npm install --save-dev @babel/core @babel/preset-react webpack webpack-cli babel-loader css-loader style-loader
|
||||
npm install cytoscape # For NetworkMap
|
||||
```
|
||||
|
||||
2. **Create webpack.config.js**
|
||||
|
||||
```javascript
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.jsx',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'static/dist'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-react']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx']
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
3. **Create src/index.jsx**
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
```
|
||||
|
||||
4. **Create src/App.jsx**
|
||||
|
||||
```jsx
|
||||
import React, { useState } from 'react';
|
||||
import NetworkMap from '../NetworkMap';
|
||||
import VoiceControls from '../VoiceControls';
|
||||
import HelpChat from '../HelpChat';
|
||||
import GuidedWizard from '../GuidedWizard';
|
||||
|
||||
function App() {
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const [showWizard, setShowWizard] = useState(false);
|
||||
const [currentScanId, setCurrentScanId] = useState(null);
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header>
|
||||
<h1>StrikePackageGPT Dashboard</h1>
|
||||
<button onClick={() => setShowHelp(!showHelp)}>
|
||||
💬 Help
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{currentScanId && (
|
||||
<NetworkMap
|
||||
scanId={currentScanId}
|
||||
onNodeClick={(host) => console.log('Host clicked:', host)}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Floating components */}
|
||||
<VoiceControls onCommand={handleVoiceCommand} />
|
||||
|
||||
<HelpChat
|
||||
isOpen={showHelp}
|
||||
onClose={() => setShowHelp(false)}
|
||||
currentPage="dashboard"
|
||||
/>
|
||||
|
||||
{showWizard && (
|
||||
<GuidedWizard
|
||||
wizardType="first_time_setup"
|
||||
onComplete={(data) => {
|
||||
console.log('Wizard completed:', data);
|
||||
setShowWizard(false);
|
||||
}}
|
||||
onCancel={() => setShowWizard(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function handleVoiceCommand(result) {
|
||||
console.log('Voice command:', result);
|
||||
// Handle voice commands
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
5. **Update package.json scripts**
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"dev": "webpack --mode development --watch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **Build and Deploy**
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
7. **Update templates/index.html**
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>StrikePackageGPT</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/static/dist/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Use Components via CDN (Quick Start)
|
||||
|
||||
For quick testing without build system:
|
||||
|
||||
1. **Create static/js/components.js**
|
||||
|
||||
```javascript
|
||||
// Load React and ReactDOM from CDN
|
||||
// Then include the component code
|
||||
|
||||
// Example: Simple integration
|
||||
function initStrikePackageGPT() {
|
||||
// Initialize voice controls
|
||||
const voiceContainer = document.createElement('div');
|
||||
voiceContainer.id = 'voice-controls';
|
||||
document.body.appendChild(voiceContainer);
|
||||
|
||||
// Initialize help chat button
|
||||
const helpButton = document.createElement('button');
|
||||
helpButton.textContent = '💬 Help';
|
||||
helpButton.onclick = () => toggleHelpChat();
|
||||
document.body.appendChild(helpButton);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initStrikePackageGPT);
|
||||
```
|
||||
|
||||
2. **Update templates/index.html**
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>StrikePackageGPT</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<!-- Include components -->
|
||||
<script type="text/babel" src="/static/js/components.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 3: Progressive Enhancement (Current Setup Compatible)
|
||||
|
||||
Use the new features as API endpoints with vanilla JavaScript:
|
||||
|
||||
1. **Create static/js/app.js**
|
||||
|
||||
```javascript
|
||||
// Voice Control Integration
|
||||
class VoiceController {
|
||||
constructor() {
|
||||
this.isListening = false;
|
||||
this.mediaRecorder = null;
|
||||
this.setupButton();
|
||||
}
|
||||
|
||||
setupButton() {
|
||||
const button = document.createElement('button');
|
||||
button.id = 'voice-button';
|
||||
button.innerHTML = '🎙️';
|
||||
button.onclick = () => this.toggleListening();
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
|
||||
async toggleListening() {
|
||||
if (!this.isListening) {
|
||||
await this.startListening();
|
||||
} else {
|
||||
this.stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
async startListening() {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
this.mediaRecorder = new MediaRecorder(stream);
|
||||
const chunks = [];
|
||||
|
||||
this.mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
|
||||
this.mediaRecorder.onstop = async () => {
|
||||
const blob = new Blob(chunks, { type: 'audio/webm' });
|
||||
await this.processAudio(blob);
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
};
|
||||
|
||||
this.mediaRecorder.start();
|
||||
this.isListening = true;
|
||||
document.getElementById('voice-button').innerHTML = '⏸️';
|
||||
}
|
||||
|
||||
stopListening() {
|
||||
if (this.mediaRecorder) {
|
||||
this.mediaRecorder.stop();
|
||||
this.isListening = false;
|
||||
document.getElementById('voice-button').innerHTML = '🎙️';
|
||||
}
|
||||
}
|
||||
|
||||
async processAudio(blob) {
|
||||
const formData = new FormData();
|
||||
formData.append('audio', blob);
|
||||
|
||||
const response = await fetch('/api/voice/transcribe', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Transcribed:', result.text);
|
||||
|
||||
// Route command
|
||||
const cmdResponse = await fetch('/api/voice/command', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: result.text })
|
||||
});
|
||||
|
||||
const command = await cmdResponse.json();
|
||||
this.executeCommand(command);
|
||||
}
|
||||
|
||||
executeCommand(command) {
|
||||
// Execute the command based on routing info
|
||||
console.log('Command:', command);
|
||||
}
|
||||
}
|
||||
|
||||
// Help Chat Integration
|
||||
class HelpChat {
|
||||
constructor() {
|
||||
this.isOpen = false;
|
||||
this.messages = [];
|
||||
this.sessionId = `session-${Date.now()}`;
|
||||
this.setupUI();
|
||||
}
|
||||
|
||||
setupUI() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'help-chat';
|
||||
container.style.display = 'none';
|
||||
document.body.appendChild(container);
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.id = 'help-button';
|
||||
button.innerHTML = '💬';
|
||||
button.onclick = () => this.toggle();
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
const chat = document.getElementById('help-chat');
|
||||
chat.style.display = this.isOpen ? 'block' : 'none';
|
||||
|
||||
if (this.isOpen && this.messages.length === 0) {
|
||||
this.addMessage('assistant', 'Hi! How can I help you?');
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(text) {
|
||||
this.addMessage('user', text);
|
||||
|
||||
const response = await fetch('/api/llm/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: text,
|
||||
session_id: this.sessionId
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
this.addMessage('assistant', result.message);
|
||||
}
|
||||
|
||||
addMessage(role, content) {
|
||||
this.messages.push({ role, content });
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const chat = document.getElementById('help-chat');
|
||||
chat.innerHTML = this.messages.map(msg => `
|
||||
<div class="message ${msg.role}">
|
||||
${msg.content}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// Network Map Integration
|
||||
class NetworkMapViewer {
|
||||
constructor(containerId) {
|
||||
this.container = document.getElementById(containerId);
|
||||
this.hosts = [];
|
||||
}
|
||||
|
||||
async loadScan(scanId) {
|
||||
const response = await fetch(`/api/nmap/hosts?scan_id=${scanId}`);
|
||||
const data = await response.json();
|
||||
this.hosts = data.hosts || [];
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.container.innerHTML = `
|
||||
<div class="network-map">
|
||||
<div class="toolbar">
|
||||
<input type="text" id="filter" placeholder="Filter..." />
|
||||
<button onclick="networkMap.exportCSV()">Export CSV</button>
|
||||
</div>
|
||||
<div class="hosts">
|
||||
${this.hosts.map(host => this.renderHost(host)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderHost(host) {
|
||||
const iconUrl = `/static/${this.getIcon(host)}.svg`;
|
||||
return `
|
||||
<div class="host" onclick="networkMap.showHostDetails('${host.ip}')">
|
||||
<img src="${iconUrl}" alt="${host.os_type}" />
|
||||
<div class="host-info">
|
||||
<strong>${host.ip}</strong>
|
||||
<div>${host.hostname || 'Unknown'}</div>
|
||||
<div>${host.os_type || 'Unknown OS'}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getIcon(host) {
|
||||
const osType = (host.os_type || '').toLowerCase();
|
||||
if (osType.includes('windows')) return 'windows';
|
||||
if (osType.includes('linux')) return 'linux';
|
||||
if (osType.includes('mac')) return 'mac';
|
||||
if (host.device_type?.includes('server')) return 'server';
|
||||
if (host.device_type?.includes('network')) return 'network';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
exportCSV() {
|
||||
const csv = [
|
||||
['IP', 'Hostname', 'OS', 'Device Type', 'Ports'].join(','),
|
||||
...this.hosts.map(h => [
|
||||
h.ip,
|
||||
h.hostname || '',
|
||||
h.os_type || '',
|
||||
h.device_type || '',
|
||||
(h.ports || []).map(p => p.port).join(';')
|
||||
].join(','))
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `network-${Date.now()}.csv`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
showHostDetails(ip) {
|
||||
const host = this.hosts.find(h => h.ip === ip);
|
||||
alert(JSON.stringify(host, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.voiceController = new VoiceController();
|
||||
window.helpChat = new HelpChat();
|
||||
window.networkMap = new NetworkMapViewer('network-map-container');
|
||||
});
|
||||
```
|
||||
|
||||
2. **Add CSS (static/css/components.css)**
|
||||
|
||||
```css
|
||||
/* Voice Button */
|
||||
#voice-button {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #3498DB;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Help Chat */
|
||||
#help-chat {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
width: 400px;
|
||||
height: 600px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||
z-index: 999;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#help-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #3498DB;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
background: #3498DB;
|
||||
color: white;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
background: #ECF0F1;
|
||||
color: #2C3E50;
|
||||
}
|
||||
|
||||
/* Network Map */
|
||||
.network-map {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.hosts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.host {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.host:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.host img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.host-info {
|
||||
margin-top: 10px;
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update templates/index.html**
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>StrikePackageGPT</title>
|
||||
<link rel="stylesheet" href="/static/css/components.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="network-map-container"></div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing the Integration
|
||||
|
||||
### Test Voice Control
|
||||
1. Open browser console
|
||||
2. Click the mic button
|
||||
3. Speak a command
|
||||
4. Check console for transcription result
|
||||
|
||||
### Test Help Chat
|
||||
1. Click the help button
|
||||
2. Type a message
|
||||
3. Wait for AI response
|
||||
|
||||
### Test Network Map
|
||||
```javascript
|
||||
// In browser console
|
||||
networkMap.loadScan('your-scan-id');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
- [ ] Choose integration method (build system vs progressive enhancement)
|
||||
- [ ] Install required npm packages (if using React build)
|
||||
- [ ] Configure API endpoints in backend
|
||||
- [ ] Add environment variables for API keys
|
||||
- [ ] Test voice control permissions
|
||||
- [ ] Verify LLM service connectivity
|
||||
- [ ] Test network map with real scan data
|
||||
- [ ] Configure CORS if needed
|
||||
- [ ] Add error handling for API failures
|
||||
- [ ] Test on multiple browsers
|
||||
- [ ] Document any additional setup steps
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Choose your integration approach
|
||||
2. Set up the build system (if needed)
|
||||
3. Test each component individually
|
||||
4. Integrate components into main dashboard
|
||||
5. Add error handling and loading states
|
||||
6. Style components to match your theme
|
||||
7. Deploy and test in production environment
|
||||
|
||||
For questions or issues, refer to FEATURES.md or use the Help Chat! 😊
|
||||
48
README.md
@@ -14,6 +14,7 @@ StrikePackageGPT provides security researchers and penetration testers with an A
|
||||
- **Vulnerability Analysis** - CVE research, misconfiguration detection
|
||||
- **Exploit Research** - Safe research and documentation of exploits
|
||||
- **Report Generation** - Professional security assessment reports
|
||||
- **🆕 Bidirectional Command Capture** - Run commands in CLI, see results in dashboard
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
@@ -64,18 +65,55 @@ StrikePackageGPT provides security researchers and penetration testers with an A
|
||||
|
||||
## 🛠️ Security Tools
|
||||
|
||||
The Kali container includes:
|
||||
The Kali container includes **ALL Kali Linux tools** via the `kali-linux-everything` metapackage:
|
||||
|
||||
- **Reconnaissance**: nmap, masscan, amass, theHarvester, whatweb
|
||||
- **Web Testing**: nikto, gobuster, dirb, sqlmap
|
||||
- **Exploitation**: metasploit-framework, hydra, searchsploit
|
||||
- **Network**: tcpdump, netcat, wireshark
|
||||
- **600+ Security Tools**: Complete Kali Linux arsenal
|
||||
- **Reconnaissance**: nmap, masscan, amass, theHarvester, whatweb, recon-ng, maltego
|
||||
- **Web Testing**: nikto, gobuster, dirb, sqlmap, burpsuite, zaproxy, wpscan
|
||||
- **Exploitation**: metasploit-framework, exploit-db, searchsploit, armitage
|
||||
- **Password Attacks**: hydra, john, hashcat, medusa, ncrack
|
||||
- **Wireless**: aircrack-ng, wifite, reaver, bully, kismet, fern-wifi-cracker
|
||||
- **Sniffing/Spoofing**: wireshark, tcpdump, ettercap, bettercap, responder
|
||||
- **Post-Exploitation**: mimikatz, powersploit, empire, covenant
|
||||
- **Forensics**: autopsy, volatility, sleuthkit, foremost
|
||||
- **Reverse Engineering**: ghidra, radare2, gdb, ollydbg, ida-free
|
||||
- **Social Engineering**: set (Social Engineering Toolkit)
|
||||
- **And hundreds more...**
|
||||
|
||||
Access the Kali container:
|
||||
```bash
|
||||
docker exec -it strikepackage-kali bash
|
||||
```
|
||||
|
||||
### 🔄 Bidirectional Command Capture
|
||||
|
||||
**New Feature!** Commands run directly in the Kali container are now automatically captured and visible in the dashboard:
|
||||
|
||||
```bash
|
||||
# Connect to container
|
||||
docker exec -it strikepackage-kali bash
|
||||
|
||||
# Run commands normally - they're automatically logged
|
||||
nmap -sV 192.168.1.0/24
|
||||
|
||||
# Use 'capture' for full output capture
|
||||
capture sqlmap -u "http://example.com?id=1" --batch
|
||||
|
||||
# View recent commands
|
||||
recent
|
||||
|
||||
# All commands appear in dashboard history! 🎉
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ Use CLI for speed, GUI for visualization
|
||||
- ✅ Perfect for advanced users who prefer terminal
|
||||
- ✅ Unified history across all command sources
|
||||
- ✅ Network map includes manually-run scans
|
||||
- ✅ Complete audit trail for reporting
|
||||
|
||||
See `BIDIRECTIONAL_CAPTURE.md` for full documentation.
|
||||
|
||||
## 🤖 LLM Providers
|
||||
|
||||
StrikePackageGPT supports multiple LLM providers:
|
||||
|
||||
522
create_and_zip.sh
Normal file
@@ -0,0 +1,522 @@
|
||||
#!/usr/bin/env bash
|
||||
# create_and_zip.sh
|
||||
# Creates the directory tree and files for the "new files from today"
|
||||
# and packages them into goose_c2_files.zip
|
||||
# Usage in iSH:
|
||||
# paste this file via heredoc, then:
|
||||
# chmod +x create_and_zip.sh
|
||||
# ./create_and_zip.sh
|
||||
set -euo pipefail
|
||||
|
||||
# Create directories (idempotent)
|
||||
mkdir -p backend/workers frontend/src/components
|
||||
|
||||
# Write backend/models.py
|
||||
cat > backend/models.py <<'PYEOF'
|
||||
# -- C2 Models Extension for GooseStrike --
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey, Table
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.types import JSON as JSONType
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
c2_agent_asset = Table(
|
||||
'c2_agent_asset', Base.metadata,
|
||||
Column('agent_id', Integer, ForeignKey('c2_agents.id')),
|
||||
Column('asset_id', Integer, ForeignKey('assets.id')),
|
||||
)
|
||||
|
||||
class C2Instance(Base):
|
||||
__tablename__ = "c2_instances"
|
||||
id = Column(Integer, primary_key=True)
|
||||
provider = Column(String)
|
||||
status = Column(String)
|
||||
last_poll = Column(DateTime)
|
||||
error = Column(Text)
|
||||
|
||||
class C2Operation(Base):
|
||||
__tablename__ = 'c2_operations'
|
||||
id = Column(Integer, primary_key=True)
|
||||
operation_id = Column(String, unique=True, index=True)
|
||||
name = Column(String)
|
||||
provider = Column(String)
|
||||
campaign_id = Column(Integer, ForeignKey("campaigns.id"), nullable=True)
|
||||
description = Column(Text)
|
||||
start_time = Column(DateTime)
|
||||
end_time = Column(DateTime)
|
||||
alerts = relationship("C2Event", backref="operation")
|
||||
|
||||
class C2Agent(Base):
|
||||
__tablename__ = 'c2_agents'
|
||||
id = Column(Integer, primary_key=True)
|
||||
agent_id = Column(String, unique=True, index=True)
|
||||
provider = Column(String)
|
||||
name = Column(String)
|
||||
operation_id = Column(Integer, ForeignKey("c2_operations.id"), nullable=True)
|
||||
first_seen = Column(DateTime)
|
||||
last_seen = Column(DateTime)
|
||||
ip_address = Column(String)
|
||||
hostname = Column(String)
|
||||
platform = Column(String)
|
||||
user = Column(String)
|
||||
pid = Column(Integer)
|
||||
state = Column(String)
|
||||
mitre_techniques = Column(JSONType)
|
||||
assets = relationship("Asset", secondary=c2_agent_asset, backref="c2_agents")
|
||||
|
||||
class C2Event(Base):
|
||||
__tablename__ = 'c2_events'
|
||||
id = Column(Integer, primary_key=True)
|
||||
event_id = Column(String, unique=True, index=True)
|
||||
type = Column(String)
|
||||
description = Column(Text)
|
||||
agent_id = Column(Integer, ForeignKey('c2_agents.id'))
|
||||
operation_id = Column(Integer, ForeignKey('c2_operations.id'))
|
||||
timestamp = Column(DateTime)
|
||||
mitre_tag = Column(String)
|
||||
details = Column(JSONType, default=dict)
|
||||
|
||||
class C2Payload(Base):
|
||||
__tablename__ = "c2_payloads"
|
||||
id = Column(Integer, primary_key=True)
|
||||
payload_id = Column(String, unique=True)
|
||||
provider = Column(String)
|
||||
agent_id = Column(String)
|
||||
operation_id = Column(String)
|
||||
type = Column(String)
|
||||
created_at = Column(DateTime)
|
||||
filename = Column(String)
|
||||
path = Column(String)
|
||||
content = Column(Text)
|
||||
|
||||
class C2Listener(Base):
|
||||
__tablename__ = "c2_listeners"
|
||||
id = Column(Integer, primary_key=True)
|
||||
listener_id = Column(String, unique=True)
|
||||
provider = Column(String)
|
||||
operation_id = Column(String)
|
||||
port = Column(Integer)
|
||||
transport = Column(String)
|
||||
status = Column(String)
|
||||
created_at = Column(DateTime)
|
||||
|
||||
class C2Task(Base):
|
||||
__tablename__ = "c2_tasks"
|
||||
id = Column(Integer, primary_key=True)
|
||||
task_id = Column(String, unique=True, index=True)
|
||||
agent_id = Column(String)
|
||||
operation_id = Column(String)
|
||||
command = Column(Text)
|
||||
status = Column(String)
|
||||
result = Column(Text)
|
||||
created_at = Column(DateTime)
|
||||
executed_at = Column(DateTime)
|
||||
error = Column(Text)
|
||||
mitre_technique = Column(String)
|
||||
PYEOF
|
||||
|
||||
# Write backend/workers/c2_integration.py
|
||||
cat > backend/workers/c2_integration.py <<'PYEOF'
|
||||
#!/usr/bin/env python3
|
||||
# Simplified C2 poller adapters (Mythic/Caldera) — adjust imports for your repo
|
||||
import os, time, requests, logging
|
||||
from datetime import datetime
|
||||
# Import models and Session from your project; this is a placeholder import
|
||||
try:
|
||||
from models import Session, C2Instance, C2Agent, C2Operation, C2Event, C2Payload, C2Listener, C2Task, Asset
|
||||
except Exception:
|
||||
# If using package layout, adapt the import path
|
||||
try:
|
||||
from backend.models import Session, C2Instance, C2Agent, C2Operation, C2Event, C2Payload, C2Listener, C2Task, Asset
|
||||
except Exception:
|
||||
# Minimal placeholders to avoid immediate runtime errors during demo
|
||||
Session = None
|
||||
C2Instance = C2Agent = C2Operation = C2Event = C2Payload = C2Listener = C2Task = Asset = object
|
||||
|
||||
from urllib.parse import urljoin
|
||||
|
||||
class BaseC2Adapter:
|
||||
def __init__(self, base_url, api_token):
|
||||
self.base_url = base_url
|
||||
self.api_token = api_token
|
||||
|
||||
def api(self, path, method="get", **kwargs):
|
||||
url = urljoin(self.base_url, path)
|
||||
headers = kwargs.pop("headers", {})
|
||||
if self.api_token:
|
||||
headers["Authorization"] = f"Bearer {self.api_token}"
|
||||
try:
|
||||
r = getattr(requests, method)(url, headers=headers, timeout=15, **kwargs)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logging.error(f"C2 API error {url}: {e}")
|
||||
return None
|
||||
|
||||
def get_status(self): raise NotImplementedError
|
||||
def get_agents(self): raise NotImplementedError
|
||||
def get_operations(self): raise NotImplementedError
|
||||
def get_events(self, since=None): raise NotImplementedError
|
||||
def create_payload(self, op_id, typ, params): raise NotImplementedError
|
||||
def launch_command(self, agent_id, cmd): raise NotImplementedError
|
||||
def create_listener(self, op_id, port, transport): raise NotImplementedError
|
||||
|
||||
class MythicAdapter(BaseC2Adapter):
|
||||
def get_status(self): return self.api("/api/v1/status")
|
||||
def get_agents(self): return (self.api("/api/v1/agents") or {}).get("agents", [])
|
||||
def get_operations(self): return (self.api("/api/v1/operations") or {}).get("operations", [])
|
||||
def get_events(self, since=None): return (self.api("/api/v1/events") or {}).get("events", [])
|
||||
def create_payload(self, op_id, typ, params):
|
||||
return self.api("/api/v1/payloads", "post", json={"operation_id": op_id, "type": typ, "params": params})
|
||||
def launch_command(self, agent_id, cmd):
|
||||
return self.api(f"/api/v1/agents/{agent_id}/tasks", "post", json={"command": cmd})
|
||||
def create_listener(self, op_id, port, transport):
|
||||
return self.api("/api/v1/listeners", "post", json={"operation_id": op_id, "port": port, "transport": transport})
|
||||
|
||||
class CalderaAdapter(BaseC2Adapter):
|
||||
def _caldera_headers(self):
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if self.api_token:
|
||||
headers["Authorization"] = f"Bearer {self.api_token}"
|
||||
return headers
|
||||
|
||||
def get_status(self):
|
||||
try:
|
||||
r = requests.get(f"{self.base_url}/api/health", headers=self._caldera_headers(), timeout=10)
|
||||
return {"provider": "caldera", "status": r.json().get("status", "healthy")}
|
||||
except Exception:
|
||||
return {"provider": "caldera", "status": "unreachable"}
|
||||
|
||||
def get_agents(self):
|
||||
r = requests.get(f"{self.base_url}/api/agents/all", headers=self._caldera_headers(), timeout=15)
|
||||
agents = r.json() if r.status_code == 200 else []
|
||||
for agent in agents:
|
||||
mitre_tids = []
|
||||
for ab in agent.get("abilities", []):
|
||||
tid = ab.get("attack", {}).get("technique_id")
|
||||
if tid:
|
||||
mitre_tids.append(tid)
|
||||
agent["mitre"] = mitre_tids
|
||||
return [{"id": agent.get("paw"), "name": agent.get("host"), "ip": agent.get("host"), "hostname": agent.get("host"), "platform": agent.get("platform"), "pid": agent.get("pid"), "status": "online" if agent.get("trusted", False) else "offline", "mitre": agent.get("mitre"), "operation": agent.get("operation")} for agent in agents]
|
||||
|
||||
def get_operations(self):
|
||||
r = requests.get(f"{self.base_url}/api/operations", headers=self._caldera_headers(), timeout=10)
|
||||
ops = r.json() if r.status_code == 200 else []
|
||||
return [{"id": op.get("id"), "name": op.get("name"), "start_time": op.get("start"), "description": op.get("description", "")} for op in ops]
|
||||
|
||||
def get_events(self, since_timestamp=None):
|
||||
events = []
|
||||
ops = self.get_operations()
|
||||
for op in ops:
|
||||
url = f"{self.base_url}/api/operations/{op['id']}/reports"
|
||||
r = requests.get(url, headers=self._caldera_headers(), timeout=15)
|
||||
reports = r.json() if r.status_code == 200 else []
|
||||
for event in reports:
|
||||
evt_time = event.get("timestamp")
|
||||
if since_timestamp and evt_time < since_timestamp:
|
||||
continue
|
||||
events.append({"id": event.get("id", ""), "type": event.get("event_type", ""), "description": event.get("message", ""), "agent": event.get("paw", None), "operation": op["id"], "time": evt_time, "mitre": event.get("ability_id", None), "details": event})
|
||||
return events
|
||||
|
||||
def create_payload(self, operation_id, payload_type, params):
|
||||
ability_id = params.get("ability_id")
|
||||
if not ability_id:
|
||||
return {"error": "ability_id required"}
|
||||
r = requests.post(f"{self.base_url}/api/abilities/{ability_id}/create_payload", headers=self._caldera_headers(), json={"operation_id": operation_id})
|
||||
j = r.json() if r.status_code == 200 else {}
|
||||
return {"id": j.get("id", ""), "filename": j.get("filename", ""), "path": j.get("path", ""), "content": j.get("content", "")}
|
||||
|
||||
def launch_command(self, agent_id, command):
|
||||
ability_id = command.get("ability_id")
|
||||
cmd_blob = command.get("cmd_blob")
|
||||
data = {"ability_id": ability_id}
|
||||
if cmd_blob:
|
||||
data["cmd"] = cmd_blob
|
||||
r = requests.post(f"{self.base_url}/api/agents/{agent_id}/task", headers=self._caldera_headers(), json=data)
|
||||
return r.json() if r.status_code in (200,201) else {"error": "failed"}
|
||||
|
||||
def create_listener(self, operation_id, port, transport):
|
||||
try:
|
||||
r = requests.post(f"{self.base_url}/api/listeners", headers=self._caldera_headers(), json={"operation_id": operation_id, "port": port, "transport": transport})
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def get_c2_adapter():
|
||||
provider = os.getenv("C2_PROVIDER", "none")
|
||||
url = os.getenv("C2_BASE_URL", "http://c2:7443")
|
||||
token = os.getenv("C2_API_TOKEN", "")
|
||||
if provider == "mythic":
|
||||
return MythicAdapter(url, token)
|
||||
if provider == "caldera":
|
||||
return CalderaAdapter(url, token)
|
||||
return None
|
||||
|
||||
class C2Poller:
|
||||
def __init__(self, poll_interval=60):
|
||||
self.adapter = get_c2_adapter()
|
||||
self.poll_interval = int(os.getenv("C2_POLL_INTERVAL", poll_interval or 60))
|
||||
self.last_event_poll = None
|
||||
|
||||
def _store(self, instance_raw, agents_raw, operations_raw, events_raw):
|
||||
# This function expects a working SQLAlchemy Session and models
|
||||
if Session is None:
|
||||
return
|
||||
db = Session()
|
||||
now = datetime.utcnow()
|
||||
inst = db.query(C2Instance).first()
|
||||
if not inst:
|
||||
inst = C2Instance(provider=instance_raw.get("provider"), status=instance_raw.get("status"), last_poll=now)
|
||||
else:
|
||||
inst.status = instance_raw.get("status")
|
||||
inst.last_poll = now
|
||||
db.add(inst)
|
||||
|
||||
opmap = {}
|
||||
for op_data in operations_raw or []:
|
||||
op = db.query(C2Operation).filter_by(operation_id=op_data["id"]).first()
|
||||
if not op:
|
||||
op = C2Operation(operation_id=op_data["id"], name=op_data.get("name"), provider=inst.provider, start_time=op_data.get("start_time"))
|
||||
db.merge(op)
|
||||
db.flush()
|
||||
opmap[op.operation_id] = op.id
|
||||
|
||||
for agent_data in agents_raw or []:
|
||||
agent = db.query(C2Agent).filter_by(agent_id=agent_data["id"]).first()
|
||||
if not agent:
|
||||
agent = C2Agent(agent_id=agent_data["id"], provider=inst.provider, name=agent_data.get("name"), first_seen=now)
|
||||
agent.last_seen = now
|
||||
agent.operation_id = opmap.get(agent_data.get("operation"))
|
||||
agent.ip_address = agent_data.get("ip")
|
||||
agent.state = agent_data.get("status", "unknown")
|
||||
agent.mitre_techniques = agent_data.get("mitre", [])
|
||||
db.merge(agent)
|
||||
db.flush()
|
||||
|
||||
for evt in events_raw or []:
|
||||
event = db.query(C2Event).filter_by(event_id=evt.get("id","")).first()
|
||||
if not event:
|
||||
event = C2Event(event_id=evt.get("id",""), type=evt.get("type",""), description=evt.get("description",""), agent_id=evt.get("agent"), operation_id=evt.get("operation"), timestamp=evt.get("time", now), mitre_tag=evt.get("mitre"), details=evt)
|
||||
db.merge(event)
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
try:
|
||||
if not self.adapter:
|
||||
time.sleep(self.poll_interval)
|
||||
continue
|
||||
instance = self.adapter.get_status()
|
||||
agents = self.adapter.get_agents()
|
||||
operations = self.adapter.get_operations()
|
||||
events = self.adapter.get_events(since=self.last_event_poll)
|
||||
self.last_event_poll = datetime.utcnow().isoformat()
|
||||
self._store(instance, agents, operations, events)
|
||||
except Exception as e:
|
||||
print("C2 poll error", e)
|
||||
time.sleep(self.poll_interval)
|
||||
|
||||
if __name__ == "__main__":
|
||||
C2Poller().run()
|
||||
PYEOF
|
||||
|
||||
# Write backend/routes/c2.py
|
||||
cat > backend/routes_c2_placeholder.py <<'PYEOF'
|
||||
# Placeholder router. In your FastAPI app, create a router that imports your adapter and DB models.
|
||||
# This file is a simple reference; integrate into your backend/routes/c2.py as needed.
|
||||
from fastapi import APIRouter, Request
|
||||
from datetime import datetime
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/status")
|
||||
def c2_status():
|
||||
return {"provider": None, "status": "not-configured", "last_poll": None}
|
||||
PYEOF
|
||||
mv backend/routes_c2_placeholder.py backend/routes_c2.py
|
||||
|
||||
# Create the frontend component file
|
||||
cat > frontend/src/components/C2Operations.jsx <<'JSEOF'
|
||||
import React, {useEffect, useState} from "react";
|
||||
export default function C2Operations() {
|
||||
const [status, setStatus] = useState({});
|
||||
const [agents, setAgents] = useState([]);
|
||||
const [ops, setOps] = useState([]);
|
||||
const [events, setEvents] = useState([]);
|
||||
const [abilityList, setAbilityList] = useState([]);
|
||||
const [showTaskDialog, setShowTaskDialog] = useState(false);
|
||||
const [taskAgentId, setTaskAgentId] = useState(null);
|
||||
const [activeOp, setActiveOp] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/c2/status").then(r=>r.json()).then(setStatus).catch(()=>{});
|
||||
fetch("/c2/operations").then(r=>r.json()).then(ops=>{
|
||||
setOps(ops); setActiveOp(ops.length ? ops[0].id : null);
|
||||
}).catch(()=>{});
|
||||
fetch("/c2/abilities").then(r=>r.json()).then(setAbilityList).catch(()=>{});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeOp) {
|
||||
fetch(`/c2/agents?operation=${activeOp}`).then(r=>r.json()).then(setAgents).catch(()=>{});
|
||||
fetch(`/c2/events?op=${activeOp}`).then(r=>r.json()).then(setEvents).catch(()=>{});
|
||||
}
|
||||
}, [activeOp]);
|
||||
|
||||
const genPayload = async () => {
|
||||
const typ = prompt("Payload type? (beacon/http etc)");
|
||||
if (!typ) return;
|
||||
const res = await fetch("/c2/payload", {
|
||||
method:"POST",headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({operation_id:activeOp,type:typ,params:{}})
|
||||
});
|
||||
alert("Payload: " + (await res.text()));
|
||||
};
|
||||
const createListener = async () => {
|
||||
const port = prompt("Port to listen on?");
|
||||
const transport = prompt("Transport? (http/smb/etc)");
|
||||
if (!port || !transport) return;
|
||||
await fetch("/c2/listener",{method:"POST",headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({operation_id:activeOp,port:Number(port),transport})
|
||||
});
|
||||
alert("Listener created!");
|
||||
};
|
||||
const openTaskDialog = (agentId) => {
|
||||
setTaskAgentId(agentId);
|
||||
setShowTaskDialog(true);
|
||||
};
|
||||
const handleTaskSend = async () => {
|
||||
const abilityId = document.getElementById("caldera_ability_select").value;
|
||||
const cmd_blob = document.getElementById("caldera_cmd_input").value;
|
||||
await fetch(`/c2/agents/${taskAgentId}/command`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type":"application/json"},
|
||||
body: JSON.stringify({command:{ability_id:abilityId, cmd_blob}})
|
||||
});
|
||||
setShowTaskDialog(false);
|
||||
alert("Task sent to agent!");
|
||||
};
|
||||
|
||||
const renderMitre = tidList => tidList ? tidList.map(tid=>
|
||||
<span style={{border:"1px solid #8cf",borderRadius:4,padding:"2px 4px",margin:"2px",background:"#eeffee"}} key={tid}>{tid}</span>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>C2 Operations ({status.provider || 'Unconfigured'})</h2>
|
||||
<div>
|
||||
<label>Operation:</label>
|
||||
<select onChange={e=>setActiveOp(e.target.value)} value={activeOp||""}>{ops.map(op=>
|
||||
<option key={op.id} value={op.id}>{op.name}</option>
|
||||
)}</select>
|
||||
<button onClick={genPayload}>Generate Payload</button>
|
||||
<button onClick={createListener}>Create Listener</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Agents</h3>
|
||||
<table border="1"><thead>
|
||||
<tr><th>Agent</th><th>IP</th><th>Hostname</th><th>Status</th><th>MITRE</th><th>Task</th></tr>
|
||||
</thead><tbody>
|
||||
{agents.map(a=>
|
||||
<tr key={a.id}>
|
||||
<td>{a.name||a.id}</td>
|
||||
<td>{a.ip}</td>
|
||||
<td>{a.hostname}</td>
|
||||
<td>{a.state}</td>
|
||||
<td>{renderMitre(a.mitre_techniques)}</td>
|
||||
<td><button onClick={()=>openTaskDialog(a.id)}>Send Cmd</button></td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody></table>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Recent Events</h3>
|
||||
<ul>
|
||||
{events.map(e=>
|
||||
<li key={e.id}>[{e.type}] {e.desc} [Agent:{e.agent} Op:{e.op}] {e.mitre && <b>{e.mitre}</b>} @ {e.time}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{display:'inline-block',background:'#ffe',border:'1px solid #ec3',padding:4,margin:4}}>⚠️ <b>LAB ONLY: All actions are for simulation/training inside this closed cyber range!</b></span>
|
||||
</div>
|
||||
{showTaskDialog &&
|
||||
<div style={{
|
||||
position: "fixed", background: "#fff", top: "20%", left: "40%",
|
||||
border: "2px solid #246", borderRadius: 8, padding: 16, zIndex: 10
|
||||
}}>
|
||||
<h3>Task Agent {taskAgentId} (Caldera)</h3>
|
||||
<label>Ability:</label>
|
||||
<select id="caldera_ability_select">
|
||||
{abilityList.map(ab =>
|
||||
<option key={ab.ability_id} value={ab.ability_id}>
|
||||
{ab.name} - {ab.attack && ab.attack.technique_id}
|
||||
</option>)}
|
||||
</select>
|
||||
<br />
|
||||
<label>Command Blob (optional):</label>
|
||||
<input id="caldera_cmd_input" placeholder="bash -c ..."/>
|
||||
<br />
|
||||
<button onClick={handleTaskSend}>Send</button>
|
||||
<button onClick={()=>setShowTaskDialog(false)}>Cancel</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
JSEOF
|
||||
|
||||
# Minimal supporting files
|
||||
cat > docker-compose.kali.yml <<'YAML'
|
||||
services:
|
||||
api:
|
||||
build: ./backend
|
||||
ui:
|
||||
build: ./frontend
|
||||
YAML
|
||||
|
||||
cat > COMPREHENSIVE_GUIDE.md <<'GUIDE'
|
||||
# Comprehensive Guide (placeholder)
|
||||
This is the comprehensive guide placeholder. Replace with full content as needed.
|
||||
GUIDE
|
||||
|
||||
cat > C2-integration-session.md <<'SESSION'
|
||||
C2 integration session transcript placeholder.
|
||||
SESSION
|
||||
|
||||
cat > README.md <<'RME'
|
||||
# GooseStrike Cyber Range - placeholder README
|
||||
RME
|
||||
|
||||
# Create a simple package.json to ensure directory present
|
||||
mkdir -p frontend
|
||||
cat > frontend/package.json <<'PKG'
|
||||
{ "name": "goosestrike-frontend", "version": "0.1.0" }
|
||||
PKG
|
||||
|
||||
# Create the zip
|
||||
ZIPNAME="goose_c2_files.zip"
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
zip -r "${ZIPNAME}" backend frontend docker-compose.kali.yml COMPREHENSIVE_GUIDE.md C2-integration-session.md README.md >/dev/null
|
||||
else
|
||||
python3 - <<PY3
|
||||
import zipfile, os
|
||||
out = "goose_c2_files.zip"
|
||||
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for root, dirs, files in os.walk("backend"):
|
||||
for f in files:
|
||||
z.write(os.path.join(root, f))
|
||||
for root, dirs, files in os.walk("frontend"):
|
||||
for f in files:
|
||||
z.write(os.path.join(root, f))
|
||||
z.write("docker-compose.kali.yml")
|
||||
z.write("COMPREHENSIVE_GUIDE.md")
|
||||
z.write("C2-integration-session.md")
|
||||
z.write("README.md")
|
||||
print("ZIP created:", out)
|
||||
PY3
|
||||
fi
|
||||
|
||||
echo "Created goose_c2_files.zip in $(pwd)"
|
||||
ls -lh goose_c2_files.zip
|
||||
@@ -29,6 +29,8 @@ services:
|
||||
environment:
|
||||
- LLM_ROUTER_URL=http://strikepackage-llm-router:8000
|
||||
- KALI_EXECUTOR_URL=http://strikepackage-kali-executor:8002
|
||||
- DEFAULT_LLM_PROVIDER=${DEFAULT_LLM_PROVIDER:-ollama}
|
||||
- DEFAULT_LLM_MODEL=${DEFAULT_LLM_MODEL:-llama3.1:latest}
|
||||
depends_on:
|
||||
- llm-router
|
||||
- kali-executor
|
||||
@@ -48,8 +50,8 @@ services:
|
||||
- KALI_CONTAINER_NAME=strikepackage-kali
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
depends_on:
|
||||
- kali
|
||||
# depends_on:
|
||||
# - kali # Temporarily disabled due to Kali mirror SSL issues
|
||||
networks:
|
||||
- strikepackage-net
|
||||
restart: unless-stopped
|
||||
@@ -65,12 +67,16 @@ 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}
|
||||
# Local Ollama endpoint (use host.docker.internal to reach host machine from container)
|
||||
- OLLAMA_LOCAL_URL=${OLLAMA_LOCAL_URL:-http://host.docker.internal:11434}
|
||||
# Network Ollama endpoints (comma-separated for multiple)
|
||||
- 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_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434}
|
||||
# Load balancing: round-robin, random, failover
|
||||
- LOAD_BALANCE_STRATEGY=${LOAD_BALANCE_STRATEGY:-round-robin}
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
networks:
|
||||
- strikepackage-net
|
||||
restart: unless-stopped
|
||||
|
||||
522
extracted/create_and_zip.sh
Normal file
@@ -0,0 +1,522 @@
|
||||
#!/usr/bin/env bash
|
||||
# create_and_zip.sh
|
||||
# Creates the directory tree and files for the "new files from today"
|
||||
# and packages them into goose_c2_files.zip
|
||||
# Usage in iSH:
|
||||
# paste this file via heredoc, then:
|
||||
# chmod +x create_and_zip.sh
|
||||
# ./create_and_zip.sh
|
||||
set -euo pipefail
|
||||
|
||||
# Create directories (idempotent)
|
||||
mkdir -p backend/workers frontend/src/components
|
||||
|
||||
# Write backend/models.py
|
||||
cat > backend/models.py <<'PYEOF'
|
||||
# -- C2 Models Extension for GooseStrike --
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey, Table
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.types import JSON as JSONType
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
c2_agent_asset = Table(
|
||||
'c2_agent_asset', Base.metadata,
|
||||
Column('agent_id', Integer, ForeignKey('c2_agents.id')),
|
||||
Column('asset_id', Integer, ForeignKey('assets.id')),
|
||||
)
|
||||
|
||||
class C2Instance(Base):
|
||||
__tablename__ = "c2_instances"
|
||||
id = Column(Integer, primary_key=True)
|
||||
provider = Column(String)
|
||||
status = Column(String)
|
||||
last_poll = Column(DateTime)
|
||||
error = Column(Text)
|
||||
|
||||
class C2Operation(Base):
|
||||
__tablename__ = 'c2_operations'
|
||||
id = Column(Integer, primary_key=True)
|
||||
operation_id = Column(String, unique=True, index=True)
|
||||
name = Column(String)
|
||||
provider = Column(String)
|
||||
campaign_id = Column(Integer, ForeignKey("campaigns.id"), nullable=True)
|
||||
description = Column(Text)
|
||||
start_time = Column(DateTime)
|
||||
end_time = Column(DateTime)
|
||||
alerts = relationship("C2Event", backref="operation")
|
||||
|
||||
class C2Agent(Base):
|
||||
__tablename__ = 'c2_agents'
|
||||
id = Column(Integer, primary_key=True)
|
||||
agent_id = Column(String, unique=True, index=True)
|
||||
provider = Column(String)
|
||||
name = Column(String)
|
||||
operation_id = Column(Integer, ForeignKey("c2_operations.id"), nullable=True)
|
||||
first_seen = Column(DateTime)
|
||||
last_seen = Column(DateTime)
|
||||
ip_address = Column(String)
|
||||
hostname = Column(String)
|
||||
platform = Column(String)
|
||||
user = Column(String)
|
||||
pid = Column(Integer)
|
||||
state = Column(String)
|
||||
mitre_techniques = Column(JSONType)
|
||||
assets = relationship("Asset", secondary=c2_agent_asset, backref="c2_agents")
|
||||
|
||||
class C2Event(Base):
|
||||
__tablename__ = 'c2_events'
|
||||
id = Column(Integer, primary_key=True)
|
||||
event_id = Column(String, unique=True, index=True)
|
||||
type = Column(String)
|
||||
description = Column(Text)
|
||||
agent_id = Column(Integer, ForeignKey('c2_agents.id'))
|
||||
operation_id = Column(Integer, ForeignKey('c2_operations.id'))
|
||||
timestamp = Column(DateTime)
|
||||
mitre_tag = Column(String)
|
||||
details = Column(JSONType, default=dict)
|
||||
|
||||
class C2Payload(Base):
|
||||
__tablename__ = "c2_payloads"
|
||||
id = Column(Integer, primary_key=True)
|
||||
payload_id = Column(String, unique=True)
|
||||
provider = Column(String)
|
||||
agent_id = Column(String)
|
||||
operation_id = Column(String)
|
||||
type = Column(String)
|
||||
created_at = Column(DateTime)
|
||||
filename = Column(String)
|
||||
path = Column(String)
|
||||
content = Column(Text)
|
||||
|
||||
class C2Listener(Base):
|
||||
__tablename__ = "c2_listeners"
|
||||
id = Column(Integer, primary_key=True)
|
||||
listener_id = Column(String, unique=True)
|
||||
provider = Column(String)
|
||||
operation_id = Column(String)
|
||||
port = Column(Integer)
|
||||
transport = Column(String)
|
||||
status = Column(String)
|
||||
created_at = Column(DateTime)
|
||||
|
||||
class C2Task(Base):
|
||||
__tablename__ = "c2_tasks"
|
||||
id = Column(Integer, primary_key=True)
|
||||
task_id = Column(String, unique=True, index=True)
|
||||
agent_id = Column(String)
|
||||
operation_id = Column(String)
|
||||
command = Column(Text)
|
||||
status = Column(String)
|
||||
result = Column(Text)
|
||||
created_at = Column(DateTime)
|
||||
executed_at = Column(DateTime)
|
||||
error = Column(Text)
|
||||
mitre_technique = Column(String)
|
||||
PYEOF
|
||||
|
||||
# Write backend/workers/c2_integration.py
|
||||
cat > backend/workers/c2_integration.py <<'PYEOF'
|
||||
#!/usr/bin/env python3
|
||||
# Simplified C2 poller adapters (Mythic/Caldera) — adjust imports for your repo
|
||||
import os, time, requests, logging
|
||||
from datetime import datetime
|
||||
# Import models and Session from your project; this is a placeholder import
|
||||
try:
|
||||
from models import Session, C2Instance, C2Agent, C2Operation, C2Event, C2Payload, C2Listener, C2Task, Asset
|
||||
except Exception:
|
||||
# If using package layout, adapt the import path
|
||||
try:
|
||||
from backend.models import Session, C2Instance, C2Agent, C2Operation, C2Event, C2Payload, C2Listener, C2Task, Asset
|
||||
except Exception:
|
||||
# Minimal placeholders to avoid immediate runtime errors during demo
|
||||
Session = None
|
||||
C2Instance = C2Agent = C2Operation = C2Event = C2Payload = C2Listener = C2Task = Asset = object
|
||||
|
||||
from urllib.parse import urljoin
|
||||
|
||||
class BaseC2Adapter:
|
||||
def __init__(self, base_url, api_token):
|
||||
self.base_url = base_url
|
||||
self.api_token = api_token
|
||||
|
||||
def api(self, path, method="get", **kwargs):
|
||||
url = urljoin(self.base_url, path)
|
||||
headers = kwargs.pop("headers", {})
|
||||
if self.api_token:
|
||||
headers["Authorization"] = f"Bearer {self.api_token}"
|
||||
try:
|
||||
r = getattr(requests, method)(url, headers=headers, timeout=15, **kwargs)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
logging.error(f"C2 API error {url}: {e}")
|
||||
return None
|
||||
|
||||
def get_status(self): raise NotImplementedError
|
||||
def get_agents(self): raise NotImplementedError
|
||||
def get_operations(self): raise NotImplementedError
|
||||
def get_events(self, since=None): raise NotImplementedError
|
||||
def create_payload(self, op_id, typ, params): raise NotImplementedError
|
||||
def launch_command(self, agent_id, cmd): raise NotImplementedError
|
||||
def create_listener(self, op_id, port, transport): raise NotImplementedError
|
||||
|
||||
class MythicAdapter(BaseC2Adapter):
|
||||
def get_status(self): return self.api("/api/v1/status")
|
||||
def get_agents(self): return (self.api("/api/v1/agents") or {}).get("agents", [])
|
||||
def get_operations(self): return (self.api("/api/v1/operations") or {}).get("operations", [])
|
||||
def get_events(self, since=None): return (self.api("/api/v1/events") or {}).get("events", [])
|
||||
def create_payload(self, op_id, typ, params):
|
||||
return self.api("/api/v1/payloads", "post", json={"operation_id": op_id, "type": typ, "params": params})
|
||||
def launch_command(self, agent_id, cmd):
|
||||
return self.api(f"/api/v1/agents/{agent_id}/tasks", "post", json={"command": cmd})
|
||||
def create_listener(self, op_id, port, transport):
|
||||
return self.api("/api/v1/listeners", "post", json={"operation_id": op_id, "port": port, "transport": transport})
|
||||
|
||||
class CalderaAdapter(BaseC2Adapter):
|
||||
def _caldera_headers(self):
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if self.api_token:
|
||||
headers["Authorization"] = f"Bearer {self.api_token}"
|
||||
return headers
|
||||
|
||||
def get_status(self):
|
||||
try:
|
||||
r = requests.get(f"{self.base_url}/api/health", headers=self._caldera_headers(), timeout=10)
|
||||
return {"provider": "caldera", "status": r.json().get("status", "healthy")}
|
||||
except Exception:
|
||||
return {"provider": "caldera", "status": "unreachable"}
|
||||
|
||||
def get_agents(self):
|
||||
r = requests.get(f"{self.base_url}/api/agents/all", headers=self._caldera_headers(), timeout=15)
|
||||
agents = r.json() if r.status_code == 200 else []
|
||||
for agent in agents:
|
||||
mitre_tids = []
|
||||
for ab in agent.get("abilities", []):
|
||||
tid = ab.get("attack", {}).get("technique_id")
|
||||
if tid:
|
||||
mitre_tids.append(tid)
|
||||
agent["mitre"] = mitre_tids
|
||||
return [{"id": agent.get("paw"), "name": agent.get("host"), "ip": agent.get("host"), "hostname": agent.get("host"), "platform": agent.get("platform"), "pid": agent.get("pid"), "status": "online" if agent.get("trusted", False) else "offline", "mitre": agent.get("mitre"), "operation": agent.get("operation")} for agent in agents]
|
||||
|
||||
def get_operations(self):
|
||||
r = requests.get(f"{self.base_url}/api/operations", headers=self._caldera_headers(), timeout=10)
|
||||
ops = r.json() if r.status_code == 200 else []
|
||||
return [{"id": op.get("id"), "name": op.get("name"), "start_time": op.get("start"), "description": op.get("description", "")} for op in ops]
|
||||
|
||||
def get_events(self, since_timestamp=None):
|
||||
events = []
|
||||
ops = self.get_operations()
|
||||
for op in ops:
|
||||
url = f"{self.base_url}/api/operations/{op['id']}/reports"
|
||||
r = requests.get(url, headers=self._caldera_headers(), timeout=15)
|
||||
reports = r.json() if r.status_code == 200 else []
|
||||
for event in reports:
|
||||
evt_time = event.get("timestamp")
|
||||
if since_timestamp and evt_time < since_timestamp:
|
||||
continue
|
||||
events.append({"id": event.get("id", ""), "type": event.get("event_type", ""), "description": event.get("message", ""), "agent": event.get("paw", None), "operation": op["id"], "time": evt_time, "mitre": event.get("ability_id", None), "details": event})
|
||||
return events
|
||||
|
||||
def create_payload(self, operation_id, payload_type, params):
|
||||
ability_id = params.get("ability_id")
|
||||
if not ability_id:
|
||||
return {"error": "ability_id required"}
|
||||
r = requests.post(f"{self.base_url}/api/abilities/{ability_id}/create_payload", headers=self._caldera_headers(), json={"operation_id": operation_id})
|
||||
j = r.json() if r.status_code == 200 else {}
|
||||
return {"id": j.get("id", ""), "filename": j.get("filename", ""), "path": j.get("path", ""), "content": j.get("content", "")}
|
||||
|
||||
def launch_command(self, agent_id, command):
|
||||
ability_id = command.get("ability_id")
|
||||
cmd_blob = command.get("cmd_blob")
|
||||
data = {"ability_id": ability_id}
|
||||
if cmd_blob:
|
||||
data["cmd"] = cmd_blob
|
||||
r = requests.post(f"{self.base_url}/api/agents/{agent_id}/task", headers=self._caldera_headers(), json=data)
|
||||
return r.json() if r.status_code in (200,201) else {"error": "failed"}
|
||||
|
||||
def create_listener(self, operation_id, port, transport):
|
||||
try:
|
||||
r = requests.post(f"{self.base_url}/api/listeners", headers=self._caldera_headers(), json={"operation_id": operation_id, "port": port, "transport": transport})
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def get_c2_adapter():
|
||||
provider = os.getenv("C2_PROVIDER", "none")
|
||||
url = os.getenv("C2_BASE_URL", "http://c2:7443")
|
||||
token = os.getenv("C2_API_TOKEN", "")
|
||||
if provider == "mythic":
|
||||
return MythicAdapter(url, token)
|
||||
if provider == "caldera":
|
||||
return CalderaAdapter(url, token)
|
||||
return None
|
||||
|
||||
class C2Poller:
|
||||
def __init__(self, poll_interval=60):
|
||||
self.adapter = get_c2_adapter()
|
||||
self.poll_interval = int(os.getenv("C2_POLL_INTERVAL", poll_interval or 60))
|
||||
self.last_event_poll = None
|
||||
|
||||
def _store(self, instance_raw, agents_raw, operations_raw, events_raw):
|
||||
# This function expects a working SQLAlchemy Session and models
|
||||
if Session is None:
|
||||
return
|
||||
db = Session()
|
||||
now = datetime.utcnow()
|
||||
inst = db.query(C2Instance).first()
|
||||
if not inst:
|
||||
inst = C2Instance(provider=instance_raw.get("provider"), status=instance_raw.get("status"), last_poll=now)
|
||||
else:
|
||||
inst.status = instance_raw.get("status")
|
||||
inst.last_poll = now
|
||||
db.add(inst)
|
||||
|
||||
opmap = {}
|
||||
for op_data in operations_raw or []:
|
||||
op = db.query(C2Operation).filter_by(operation_id=op_data["id"]).first()
|
||||
if not op:
|
||||
op = C2Operation(operation_id=op_data["id"], name=op_data.get("name"), provider=inst.provider, start_time=op_data.get("start_time"))
|
||||
db.merge(op)
|
||||
db.flush()
|
||||
opmap[op.operation_id] = op.id
|
||||
|
||||
for agent_data in agents_raw or []:
|
||||
agent = db.query(C2Agent).filter_by(agent_id=agent_data["id"]).first()
|
||||
if not agent:
|
||||
agent = C2Agent(agent_id=agent_data["id"], provider=inst.provider, name=agent_data.get("name"), first_seen=now)
|
||||
agent.last_seen = now
|
||||
agent.operation_id = opmap.get(agent_data.get("operation"))
|
||||
agent.ip_address = agent_data.get("ip")
|
||||
agent.state = agent_data.get("status", "unknown")
|
||||
agent.mitre_techniques = agent_data.get("mitre", [])
|
||||
db.merge(agent)
|
||||
db.flush()
|
||||
|
||||
for evt in events_raw or []:
|
||||
event = db.query(C2Event).filter_by(event_id=evt.get("id","")).first()
|
||||
if not event:
|
||||
event = C2Event(event_id=evt.get("id",""), type=evt.get("type",""), description=evt.get("description",""), agent_id=evt.get("agent"), operation_id=evt.get("operation"), timestamp=evt.get("time", now), mitre_tag=evt.get("mitre"), details=evt)
|
||||
db.merge(event)
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
try:
|
||||
if not self.adapter:
|
||||
time.sleep(self.poll_interval)
|
||||
continue
|
||||
instance = self.adapter.get_status()
|
||||
agents = self.adapter.get_agents()
|
||||
operations = self.adapter.get_operations()
|
||||
events = self.adapter.get_events(since=self.last_event_poll)
|
||||
self.last_event_poll = datetime.utcnow().isoformat()
|
||||
self._store(instance, agents, operations, events)
|
||||
except Exception as e:
|
||||
print("C2 poll error", e)
|
||||
time.sleep(self.poll_interval)
|
||||
|
||||
if __name__ == "__main__":
|
||||
C2Poller().run()
|
||||
PYEOF
|
||||
|
||||
# Write backend/routes/c2.py
|
||||
cat > backend/routes_c2_placeholder.py <<'PYEOF'
|
||||
# Placeholder router. In your FastAPI app, create a router that imports your adapter and DB models.
|
||||
# This file is a simple reference; integrate into your backend/routes/c2.py as needed.
|
||||
from fastapi import APIRouter, Request
|
||||
from datetime import datetime
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/status")
|
||||
def c2_status():
|
||||
return {"provider": None, "status": "not-configured", "last_poll": None}
|
||||
PYEOF
|
||||
mv backend/routes_c2_placeholder.py backend/routes_c2.py
|
||||
|
||||
# Create the frontend component file
|
||||
cat > frontend/src/components/C2Operations.jsx <<'JSEOF'
|
||||
import React, {useEffect, useState} from "react";
|
||||
export default function C2Operations() {
|
||||
const [status, setStatus] = useState({});
|
||||
const [agents, setAgents] = useState([]);
|
||||
const [ops, setOps] = useState([]);
|
||||
const [events, setEvents] = useState([]);
|
||||
const [abilityList, setAbilityList] = useState([]);
|
||||
const [showTaskDialog, setShowTaskDialog] = useState(false);
|
||||
const [taskAgentId, setTaskAgentId] = useState(null);
|
||||
const [activeOp, setActiveOp] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/c2/status").then(r=>r.json()).then(setStatus).catch(()=>{});
|
||||
fetch("/c2/operations").then(r=>r.json()).then(ops=>{
|
||||
setOps(ops); setActiveOp(ops.length ? ops[0].id : null);
|
||||
}).catch(()=>{});
|
||||
fetch("/c2/abilities").then(r=>r.json()).then(setAbilityList).catch(()=>{});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeOp) {
|
||||
fetch(`/c2/agents?operation=${activeOp}`).then(r=>r.json()).then(setAgents).catch(()=>{});
|
||||
fetch(`/c2/events?op=${activeOp}`).then(r=>r.json()).then(setEvents).catch(()=>{});
|
||||
}
|
||||
}, [activeOp]);
|
||||
|
||||
const genPayload = async () => {
|
||||
const typ = prompt("Payload type? (beacon/http etc)");
|
||||
if (!typ) return;
|
||||
const res = await fetch("/c2/payload", {
|
||||
method:"POST",headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({operation_id:activeOp,type:typ,params:{}})
|
||||
});
|
||||
alert("Payload: " + (await res.text()));
|
||||
};
|
||||
const createListener = async () => {
|
||||
const port = prompt("Port to listen on?");
|
||||
const transport = prompt("Transport? (http/smb/etc)");
|
||||
if (!port || !transport) return;
|
||||
await fetch("/c2/listener",{method:"POST",headers:{"Content-Type":"application/json"},
|
||||
body:JSON.stringify({operation_id:activeOp,port:Number(port),transport})
|
||||
});
|
||||
alert("Listener created!");
|
||||
};
|
||||
const openTaskDialog = (agentId) => {
|
||||
setTaskAgentId(agentId);
|
||||
setShowTaskDialog(true);
|
||||
};
|
||||
const handleTaskSend = async () => {
|
||||
const abilityId = document.getElementById("caldera_ability_select").value;
|
||||
const cmd_blob = document.getElementById("caldera_cmd_input").value;
|
||||
await fetch(`/c2/agents/${taskAgentId}/command`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type":"application/json"},
|
||||
body: JSON.stringify({command:{ability_id:abilityId, cmd_blob}})
|
||||
});
|
||||
setShowTaskDialog(false);
|
||||
alert("Task sent to agent!");
|
||||
};
|
||||
|
||||
const renderMitre = tidList => tidList ? tidList.map(tid=>
|
||||
<span style={{border:"1px solid #8cf",borderRadius:4,padding:"2px 4px",margin:"2px",background:"#eeffee"}} key={tid}>{tid}</span>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>C2 Operations ({status.provider || 'Unconfigured'})</h2>
|
||||
<div>
|
||||
<label>Operation:</label>
|
||||
<select onChange={e=>setActiveOp(e.target.value)} value={activeOp||""}>{ops.map(op=>
|
||||
<option key={op.id} value={op.id}>{op.name}</option>
|
||||
)}</select>
|
||||
<button onClick={genPayload}>Generate Payload</button>
|
||||
<button onClick={createListener}>Create Listener</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Agents</h3>
|
||||
<table border="1"><thead>
|
||||
<tr><th>Agent</th><th>IP</th><th>Hostname</th><th>Status</th><th>MITRE</th><th>Task</th></tr>
|
||||
</thead><tbody>
|
||||
{agents.map(a=>
|
||||
<tr key={a.id}>
|
||||
<td>{a.name||a.id}</td>
|
||||
<td>{a.ip}</td>
|
||||
<td>{a.hostname}</td>
|
||||
<td>{a.state}</td>
|
||||
<td>{renderMitre(a.mitre_techniques)}</td>
|
||||
<td><button onClick={()=>openTaskDialog(a.id)}>Send Cmd</button></td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody></table>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Recent Events</h3>
|
||||
<ul>
|
||||
{events.map(e=>
|
||||
<li key={e.id}>[{e.type}] {e.desc} [Agent:{e.agent} Op:{e.op}] {e.mitre && <b>{e.mitre}</b>} @ {e.time}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{display:'inline-block',background:'#ffe',border:'1px solid #ec3',padding:4,margin:4}}>⚠️ <b>LAB ONLY: All actions are for simulation/training inside this closed cyber range!</b></span>
|
||||
</div>
|
||||
{showTaskDialog &&
|
||||
<div style={{
|
||||
position: "fixed", background: "#fff", top: "20%", left: "40%",
|
||||
border: "2px solid #246", borderRadius: 8, padding: 16, zIndex: 10
|
||||
}}>
|
||||
<h3>Task Agent {taskAgentId} (Caldera)</h3>
|
||||
<label>Ability:</label>
|
||||
<select id="caldera_ability_select">
|
||||
{abilityList.map(ab =>
|
||||
<option key={ab.ability_id} value={ab.ability_id}>
|
||||
{ab.name} - {ab.attack && ab.attack.technique_id}
|
||||
</option>)}
|
||||
</select>
|
||||
<br />
|
||||
<label>Command Blob (optional):</label>
|
||||
<input id="caldera_cmd_input" placeholder="bash -c ..."/>
|
||||
<br />
|
||||
<button onClick={handleTaskSend}>Send</button>
|
||||
<button onClick={()=>setShowTaskDialog(false)}>Cancel</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
JSEOF
|
||||
|
||||
# Minimal supporting files
|
||||
cat > docker-compose.kali.yml <<'YAML'
|
||||
services:
|
||||
api:
|
||||
build: ./backend
|
||||
ui:
|
||||
build: ./frontend
|
||||
YAML
|
||||
|
||||
cat > COMPREHENSIVE_GUIDE.md <<'GUIDE'
|
||||
# Comprehensive Guide (placeholder)
|
||||
This is the comprehensive guide placeholder. Replace with full content as needed.
|
||||
GUIDE
|
||||
|
||||
cat > C2-integration-session.md <<'SESSION'
|
||||
C2 integration session transcript placeholder.
|
||||
SESSION
|
||||
|
||||
cat > README.md <<'RME'
|
||||
# GooseStrike Cyber Range - placeholder README
|
||||
RME
|
||||
|
||||
# Create a simple package.json to ensure directory present
|
||||
mkdir -p frontend
|
||||
cat > frontend/package.json <<'PKG'
|
||||
{ "name": "goosestrike-frontend", "version": "0.1.0" }
|
||||
PKG
|
||||
|
||||
# Create the zip
|
||||
ZIPNAME="goose_c2_files.zip"
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
zip -r "${ZIPNAME}" backend frontend docker-compose.kali.yml COMPREHENSIVE_GUIDE.md C2-integration-session.md README.md >/dev/null
|
||||
else
|
||||
python3 - <<PY3
|
||||
import zipfile, os
|
||||
out = "goose_c2_files.zip"
|
||||
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for root, dirs, files in os.walk("backend"):
|
||||
for f in files:
|
||||
z.write(os.path.join(root, f))
|
||||
for root, dirs, files in os.walk("frontend"):
|
||||
for f in files:
|
||||
z.write(os.path.join(root, f))
|
||||
z.write("docker-compose.kali.yml")
|
||||
z.write("COMPREHENSIVE_GUIDE.md")
|
||||
z.write("C2-integration-session.md")
|
||||
z.write("README.md")
|
||||
print("ZIP created:", out)
|
||||
PY3
|
||||
fi
|
||||
|
||||
echo "Created goose_c2_files.zip in $(pwd)"
|
||||
ls -lh goose_c2_files.zip
|
||||
49
extracted/ish_setup_and_run.sh
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env sh
|
||||
# iSH helper: install deps, run create_and_zip.sh, optionally run upload_repo.py
|
||||
# Save this as ish_setup_and_run.sh, then:
|
||||
# chmod +x ish_setup_and_run.sh
|
||||
# ./ish_setup_and_run.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "Updating apk index..."
|
||||
apk update
|
||||
|
||||
echo "Installing packages: python3, py3-pip, zip, unzip, curl, git, bash"
|
||||
apk add --no-cache python3 py3-pip zip unzip curl git bash
|
||||
|
||||
# Ensure pip and requests are available
|
||||
python3 -m ensurepip || true
|
||||
pip3 install --no-cache-dir requests
|
||||
|
||||
echo "All dependencies installed."
|
||||
|
||||
echo
|
||||
echo "FILES: place create_and_zip.sh and upload_repo.py in the current directory."
|
||||
echo "Two ways to create files in iSH:"
|
||||
echo " 1) On iPad: open this chat in Safari side-by-side with iSH, copy the script text, then run:"
|
||||
echo " cat > create_and_zip.sh <<'EOF'"
|
||||
echo " (paste content)"
|
||||
echo " EOF"
|
||||
echo " then chmod +x create_and_zip.sh"
|
||||
echo " 2) Or, use nano/vi if you installed an editor: apk add nano; nano create_and_zip.sh"
|
||||
echo
|
||||
echo "If you already have create_and_zip.sh, run:"
|
||||
echo " chmod +x create_and_zip.sh"
|
||||
echo " ./create_and_zip.sh"
|
||||
echo
|
||||
echo "After the zip is created (goose_c2_files.zip), you can either:"
|
||||
echo " - Upload from iSH to GitHub directly with upload_repo.py (preferred):"
|
||||
echo " export GITHUB_TOKEN='<your PAT>'"
|
||||
echo " export REPO='owner/repo' # e.g. mblanke/StrikePackageGPT-Lab"
|
||||
echo " export BRANCH='c2-integration' # optional"
|
||||
echo " export ZIP_FILENAME='goose_c2_files.zip'"
|
||||
echo " python3 upload_repo.py"
|
||||
echo
|
||||
echo " - Or download the zip to your iPad using a simple HTTP server:"
|
||||
echo " python3 -m http.server 8000 &"
|
||||
echo " Then open Safari and go to http://127.0.0.1:8000 to tap and download goose_c2_files.zip"
|
||||
echo
|
||||
echo "Note: iSH storage is in-app. If you want the zip in Files app, use the HTTP server method and save from Safari, or upload to Replit/GitHub directly from iSH."
|
||||
echo
|
||||
echo "Done. If you want I can paste create_and_zip.sh and upload_repo.py here for you to paste into iSH."
|
||||
134
extracted/upload_repo.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
upload_repo.py
|
||||
|
||||
Uploads files from a zip into a GitHub repo branch using the Contents API.
|
||||
|
||||
Environment variables:
|
||||
GITHUB_TOKEN - personal access token (repo scope)
|
||||
REPO - owner/repo (e.g. mblanke/StrikePackageGPT-Lab)
|
||||
BRANCH - target branch name (default: c2-integration)
|
||||
ZIP_FILENAME - name of zip file present in the current directory
|
||||
|
||||
Usage:
|
||||
export GITHUB_TOKEN='ghp_xxx'
|
||||
export REPO='owner/repo'
|
||||
export BRANCH='c2-integration'
|
||||
export ZIP_FILENAME='goose_c2_files.zip'
|
||||
python3 upload_repo.py
|
||||
"""
|
||||
import os, sys, base64, zipfile, requests, time
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
API_BASE = "https://api.github.com"
|
||||
|
||||
def die(msg):
|
||||
print("ERROR:", msg); sys.exit(1)
|
||||
|
||||
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
|
||||
REPO = os.environ.get("REPO")
|
||||
BRANCH = os.environ.get("BRANCH", "c2-integration")
|
||||
ZIP_FILENAME = os.environ.get("ZIP_FILENAME")
|
||||
|
||||
def api_headers():
|
||||
if not GITHUB_TOKEN:
|
||||
die("GITHUB_TOKEN not set")
|
||||
return {"Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json"}
|
||||
|
||||
def get_default_branch():
|
||||
url = f"{API_BASE}/repos/{REPO}"
|
||||
r = requests.get(url, headers=api_headers())
|
||||
if r.status_code != 200:
|
||||
die(f"Failed to get repo info: {r.status_code} {r.text}")
|
||||
return r.json().get("default_branch")
|
||||
|
||||
def get_ref_sha(branch):
|
||||
url = f"{API_BASE}/repos/{REPO}/git/refs/heads/{branch}"
|
||||
r = requests.get(url, headers=api_headers())
|
||||
if r.status_code == 200:
|
||||
return r.json()["object"]["sha"]
|
||||
return None
|
||||
|
||||
def create_branch(new_branch, from_sha):
|
||||
url = f"{API_BASE}/repos/{REPO}/git/refs"
|
||||
payload = {"ref": f"refs/heads/{new_branch}", "sha": from_sha}
|
||||
r = requests.post(url, json=payload, headers=api_headers())
|
||||
if r.status_code in (201, 422):
|
||||
print(f"Branch {new_branch} created or already exists.")
|
||||
return True
|
||||
else:
|
||||
die(f"Failed to create branch: {r.status_code} {r.text}")
|
||||
|
||||
def get_file_sha(path, branch):
|
||||
url = f"{API_BASE}/repos/{REPO}/contents/{quote_plus(path)}?ref={branch}"
|
||||
r = requests.get(url, headers=api_headers())
|
||||
if r.status_code == 200:
|
||||
return r.json().get("sha")
|
||||
return None
|
||||
|
||||
def put_file(path, content_b64, message, branch, sha=None):
|
||||
url = f"{API_BASE}/repos/{REPO}/contents/{quote_plus(path)}"
|
||||
payload = {"message": message, "content": content_b64, "branch": branch}
|
||||
if sha:
|
||||
payload["sha"] = sha
|
||||
r = requests.put(url, json=payload, headers=api_headers())
|
||||
return (r.status_code in (200,201)), r.text
|
||||
|
||||
def extract_zip(zip_path, target_dir):
|
||||
with zipfile.ZipFile(zip_path, 'r') as z:
|
||||
z.extractall(target_dir)
|
||||
|
||||
def gather_files(root_dir):
|
||||
files = []
|
||||
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||||
if ".git" in dirpath.split(os.sep):
|
||||
continue
|
||||
for fn in filenames:
|
||||
files.append(os.path.join(dirpath, fn))
|
||||
return files
|
||||
|
||||
def main():
|
||||
if not GITHUB_TOKEN or not REPO or not ZIP_FILENAME:
|
||||
print("Set env vars: GITHUB_TOKEN, REPO, ZIP_FILENAME. Optionally BRANCH.")
|
||||
sys.exit(1)
|
||||
if not os.path.exists(ZIP_FILENAME):
|
||||
die(f"Zip file not found: {ZIP_FILENAME}")
|
||||
default_branch = get_default_branch()
|
||||
print("Default branch:", default_branch)
|
||||
base_sha = get_ref_sha(default_branch)
|
||||
if not base_sha:
|
||||
die(f"Could not find ref for default branch {default_branch}")
|
||||
create_branch(BRANCH, base_sha)
|
||||
tmp_dir = Path("tmp_upload")
|
||||
if tmp_dir.exists():
|
||||
for p in tmp_dir.rglob("*"):
|
||||
try:
|
||||
if p.is_file(): p.unlink()
|
||||
except: pass
|
||||
tmp_dir.mkdir(exist_ok=True)
|
||||
print("Extracting zip...")
|
||||
extract_zip(ZIP_FILENAME, str(tmp_dir))
|
||||
files = gather_files(str(tmp_dir))
|
||||
print(f"Found {len(files)} files to upload")
|
||||
uploaded = 0
|
||||
for fpath in files:
|
||||
rel = os.path.relpath(fpath, str(tmp_dir))
|
||||
rel_posix = Path(rel).as_posix()
|
||||
with open(fpath, "rb") as fh:
|
||||
data = fh.read()
|
||||
content_b64 = base64.b64encode(data).decode("utf-8")
|
||||
sha = get_file_sha(rel_posix, BRANCH)
|
||||
msg = f"Add/update {rel_posix} via uploader"
|
||||
ok, resp = put_file(rel_posix, content_b64, msg, BRANCH, sha=sha)
|
||||
if ok:
|
||||
uploaded += 1
|
||||
print(f"[{uploaded}/{len(files)}] Uploaded: {rel_posix}")
|
||||
else:
|
||||
print(f"[!] Failed: {rel_posix} - {resp}")
|
||||
time.sleep(0.25)
|
||||
print(f"Completed. Uploaded {uploaded} files to branch {BRANCH}.")
|
||||
print(f"Open PR: https://github.com/{REPO}/compare/{BRANCH}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
26
extracted/upload_repo_diag.py
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
import os, sys
|
||||
from pathlib import Path
|
||||
|
||||
def env(k):
|
||||
v = os.environ.get(k)
|
||||
return "<SET>" if v else "<NOT SET>"
|
||||
|
||||
print("Python:", sys.version.splitlines()[0])
|
||||
print("PWD:", os.getcwd())
|
||||
print("Workspace files:")
|
||||
for p in Path(".").iterdir():
|
||||
print(" -", p)
|
||||
|
||||
print("\nImportant env vars:")
|
||||
for k in ("GITHUB_TOKEN","REPO","BRANCH","ZIP_FILENAME"):
|
||||
print(f" {k}: {env(k)}")
|
||||
|
||||
print("\nAttempting to read ZIP_FILENAME if set...")
|
||||
zipf = os.environ.get("ZIP_FILENAME")
|
||||
if zipf:
|
||||
p = Path(zipf)
|
||||
print("ZIP path:", p.resolve())
|
||||
print("Exists:", p.exists(), "Size:", p.stat().st_size if p.exists() else "N/A")
|
||||
else:
|
||||
print("ZIP_FILENAME not set; cannot check file.")
|
||||
49
ish_setup_and_run.sh
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env sh
|
||||
# iSH helper: install deps, run create_and_zip.sh, optionally run upload_repo.py
|
||||
# Save this as ish_setup_and_run.sh, then:
|
||||
# chmod +x ish_setup_and_run.sh
|
||||
# ./ish_setup_and_run.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "Updating apk index..."
|
||||
apk update
|
||||
|
||||
echo "Installing packages: python3, py3-pip, zip, unzip, curl, git, bash"
|
||||
apk add --no-cache python3 py3-pip zip unzip curl git bash
|
||||
|
||||
# Ensure pip and requests are available
|
||||
python3 -m ensurepip || true
|
||||
pip3 install --no-cache-dir requests
|
||||
|
||||
echo "All dependencies installed."
|
||||
|
||||
echo
|
||||
echo "FILES: place create_and_zip.sh and upload_repo.py in the current directory."
|
||||
echo "Two ways to create files in iSH:"
|
||||
echo " 1) On iPad: open this chat in Safari side-by-side with iSH, copy the script text, then run:"
|
||||
echo " cat > create_and_zip.sh <<'EOF'"
|
||||
echo " (paste content)"
|
||||
echo " EOF"
|
||||
echo " then chmod +x create_and_zip.sh"
|
||||
echo " 2) Or, use nano/vi if you installed an editor: apk add nano; nano create_and_zip.sh"
|
||||
echo
|
||||
echo "If you already have create_and_zip.sh, run:"
|
||||
echo " chmod +x create_and_zip.sh"
|
||||
echo " ./create_and_zip.sh"
|
||||
echo
|
||||
echo "After the zip is created (goose_c2_files.zip), you can either:"
|
||||
echo " - Upload from iSH to GitHub directly with upload_repo.py (preferred):"
|
||||
echo " export GITHUB_TOKEN='<your PAT>'"
|
||||
echo " export REPO='owner/repo' # e.g. mblanke/StrikePackageGPT-Lab"
|
||||
echo " export BRANCH='c2-integration' # optional"
|
||||
echo " export ZIP_FILENAME='goose_c2_files.zip'"
|
||||
echo " python3 upload_repo.py"
|
||||
echo
|
||||
echo " - Or download the zip to your iPad using a simple HTTP server:"
|
||||
echo " python3 -m http.server 8000 &"
|
||||
echo " Then open Safari and go to http://127.0.0.1:8000 to tap and download goose_c2_files.zip"
|
||||
echo
|
||||
echo "Note: iSH storage is in-app. If you want the zip in Files app, use the HTTP server method and save from Safari, or upload to Replit/GitHub directly from iSH."
|
||||
echo
|
||||
echo "Done. If you want I can paste create_and_zip.sh and upload_repo.py here for you to paste into iSH."
|
||||
345
services/dashboard/ExplainButton.jsx
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* ExplainButton Component
|
||||
* Reusable inline "Explain" button for configs, logs, and errors
|
||||
* Shows modal/popover with LLM-powered explanation
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const ExplainButton = ({
|
||||
type = 'config', // config, log, error, scan_result
|
||||
content,
|
||||
context = {},
|
||||
size = 'small',
|
||||
style = {}
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [explanation, setExplanation] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleExplain = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setShowModal(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/explain', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type,
|
||||
content,
|
||||
context
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get explanation');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setExplanation(data);
|
||||
} catch (err) {
|
||||
console.error('Error getting explanation:', err);
|
||||
setError('Failed to load explanation. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setShowModal(false);
|
||||
setExplanation(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const buttonSizes = {
|
||||
small: { padding: '4px 8px', fontSize: '12px' },
|
||||
medium: { padding: '6px 12px', fontSize: '14px' },
|
||||
large: { padding: '8px 16px', fontSize: '16px' }
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
...buttonSizes[size],
|
||||
backgroundColor: '#3498DB',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
transition: 'background-color 0.2s',
|
||||
...style
|
||||
};
|
||||
|
||||
const renderExplanation = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ color: '#E74C3C', padding: '10px' }}>
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '24px', marginBottom: '10px' }}>⏳</div>
|
||||
<div>Generating explanation...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!explanation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Render based on explanation type
|
||||
switch (type) {
|
||||
case 'config':
|
||||
return (
|
||||
<div style={{ padding: '15px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0', fontSize: '18px' }}>
|
||||
{explanation.config_key || 'Configuration'}
|
||||
</h3>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<strong>Current Value:</strong>
|
||||
<code style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '3px',
|
||||
marginLeft: '5px'
|
||||
}}>
|
||||
{explanation.current_value}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px', lineHeight: '1.6' }}>
|
||||
<strong>What it does:</strong>
|
||||
<p style={{ margin: '5px 0' }}>{explanation.description}</p>
|
||||
</div>
|
||||
|
||||
{explanation.example && (
|
||||
<div style={{ marginBottom: '15px', lineHeight: '1.6' }}>
|
||||
<strong>Example:</strong>
|
||||
<p style={{ margin: '5px 0', fontStyle: 'italic' }}>{explanation.example}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{explanation.value_analysis && (
|
||||
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#E8F4F8', borderRadius: '4px' }}>
|
||||
<strong>Analysis:</strong> {explanation.value_analysis}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{explanation.recommendations && explanation.recommendations.length > 0 && (
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<strong>Recommendations:</strong>
|
||||
<ul style={{ margin: '5px 0', paddingLeft: '20px' }}>
|
||||
{explanation.recommendations.map((rec, i) => (
|
||||
<li key={i} style={{ margin: '5px 0' }}>{rec}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '15px', paddingTop: '15px', borderTop: '1px solid #ddd' }}>
|
||||
{explanation.requires_restart && (
|
||||
<div>⚠️ Changing this setting requires a restart</div>
|
||||
)}
|
||||
{!explanation.safe_to_change && (
|
||||
<div>⚠️ Use caution when changing this setting</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'error':
|
||||
return (
|
||||
<div style={{ padding: '15px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0', fontSize: '18px', color: '#E74C3C' }}>
|
||||
Error Explanation
|
||||
</h3>
|
||||
|
||||
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#fef5e7', borderRadius: '4px', fontSize: '14px' }}>
|
||||
<strong>Original Error:</strong>
|
||||
<div style={{ marginTop: '5px', fontFamily: 'monospace', fontSize: '12px' }}>
|
||||
{explanation.original_error}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<strong>What went wrong:</strong>
|
||||
<p style={{ margin: '5px 0', lineHeight: '1.6' }}>{explanation.plain_english}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<strong>Likely causes:</strong>
|
||||
<ul style={{ margin: '5px 0', paddingLeft: '20px' }}>
|
||||
{explanation.likely_causes?.map((cause, i) => (
|
||||
<li key={i} style={{ margin: '5px 0' }}>{cause}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#E8F8F5', borderRadius: '4px' }}>
|
||||
<strong>💡 How to fix it:</strong>
|
||||
<ol style={{ margin: '5px 0', paddingLeft: '20px' }}>
|
||||
{explanation.suggested_fixes?.map((fix, i) => (
|
||||
<li key={i} style={{ margin: '5px 0' }}>{fix}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '15px' }}>
|
||||
Severity: <span style={{
|
||||
color: explanation.severity === 'critical' ? '#E74C3C' :
|
||||
explanation.severity === 'high' ? '#E67E22' :
|
||||
explanation.severity === 'medium' ? '#F39C12' : '#95A5A6',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{(explanation.severity || 'unknown').toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'log':
|
||||
return (
|
||||
<div style={{ padding: '15px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0', fontSize: '18px' }}>
|
||||
Log Entry Explanation
|
||||
</h3>
|
||||
|
||||
<div style={{ marginBottom: '15px', padding: '10px', backgroundColor: '#f5f5f5', borderRadius: '4px', fontSize: '13px', fontFamily: 'monospace' }}>
|
||||
{explanation.log_entry}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<strong>Level:</strong>
|
||||
<span style={{
|
||||
marginLeft: '5px',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '3px',
|
||||
backgroundColor: explanation.log_level === 'ERROR' ? '#E74C3C' :
|
||||
explanation.log_level === 'WARNING' ? '#F39C12' :
|
||||
explanation.log_level === 'INFO' ? '#3498DB' : '#95A5A6',
|
||||
color: 'white',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{explanation.log_level}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{explanation.timestamp && (
|
||||
<div style={{ marginBottom: '15px', fontSize: '14px', color: '#666' }}>
|
||||
<strong>Time:</strong> {explanation.timestamp}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '15px', lineHeight: '1.6' }}>
|
||||
<strong>What this means:</strong>
|
||||
<p style={{ margin: '5px 0' }}>{explanation.explanation}</p>
|
||||
</div>
|
||||
|
||||
{explanation.action_needed && explanation.next_steps && explanation.next_steps.length > 0 && (
|
||||
<div style={{ padding: '10px', backgroundColor: '#FEF5E7', borderRadius: '4px' }}>
|
||||
<strong>⚠️ Action needed:</strong>
|
||||
<ul style={{ margin: '5px 0', paddingLeft: '20px' }}>
|
||||
{explanation.next_steps.map((step, i) => (
|
||||
<li key={i} style={{ margin: '5px 0' }}>{step}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<div style={{ padding: '15px' }}>
|
||||
<div>{explanation.explanation || 'No explanation available.'}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={handleExplain}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#2980B9'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = '#3498DB'}
|
||||
style={buttonStyle}
|
||||
title="Get AI-powered explanation"
|
||||
>
|
||||
<span>❓</span>
|
||||
<span>Explain</span>
|
||||
</button>
|
||||
|
||||
{showModal && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999
|
||||
}}
|
||||
onClick={closeModal}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '600px',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
|
||||
position: 'relative'
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
backgroundColor: 'white',
|
||||
padding: '15px',
|
||||
borderBottom: '1px solid #ddd',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<h2 style={{ margin: 0, fontSize: '20px' }}>Explanation</h2>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
color: '#666'
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{renderExplanation()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExplainButton;
|
||||
487
services/dashboard/GuidedWizard.jsx
Normal file
@@ -0,0 +1,487 @@
|
||||
/**
|
||||
* GuidedWizard Component
|
||||
* Multi-step wizard for onboarding flows
|
||||
* Types: create_operation, onboard_agent, run_scan, first_time_setup
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const GuidedWizard = ({
|
||||
wizardType = 'first_time_setup',
|
||||
onComplete,
|
||||
onCancel,
|
||||
initialData = {}
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [formData, setFormData] = useState(initialData);
|
||||
const [stepHelp, setStepHelp] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const wizardConfigs = {
|
||||
create_operation: {
|
||||
title: 'Create New Operation',
|
||||
steps: [
|
||||
{
|
||||
number: 1,
|
||||
title: 'Operation Name and Type',
|
||||
fields: [
|
||||
{ name: 'operation_name', label: 'Operation Name', type: 'text', required: true, placeholder: 'Q4 Security Assessment' },
|
||||
{ name: 'operation_type', label: 'Operation Type', type: 'select', required: true, options: [
|
||||
{ value: 'external', label: 'External Penetration Test' },
|
||||
{ value: 'internal', label: 'Internal Network Assessment' },
|
||||
{ value: 'webapp', label: 'Web Application Test' },
|
||||
{ value: 'wireless', label: 'Wireless Security Assessment' }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: 'Define Target Scope',
|
||||
fields: [
|
||||
{ name: 'target_range', label: 'Target Network Range', type: 'text', required: true, placeholder: '192.168.1.0/24' },
|
||||
{ name: 'excluded_hosts', label: 'Excluded Hosts (comma-separated)', type: 'text', placeholder: '192.168.1.1, 192.168.1.254' },
|
||||
{ name: 'domains', label: 'Target Domains', type: 'textarea', placeholder: 'example.com\napp.example.com' }
|
||||
]
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: 'Configure Assessment Tools',
|
||||
fields: [
|
||||
{ name: 'scan_intensity', label: 'Scan Intensity', type: 'select', required: true, options: [
|
||||
{ value: '1', label: 'Stealth (Slowest, least detectable)' },
|
||||
{ value: '3', label: 'Balanced (Recommended)' },
|
||||
{ value: '5', label: 'Aggressive (Fastest, easily detected)' }
|
||||
]},
|
||||
{ name: 'tools', label: 'Tools to Use', type: 'multiselect', options: [
|
||||
{ value: 'nmap', label: 'Nmap (Network Scanning)' },
|
||||
{ value: 'nikto', label: 'Nikto (Web Server Scanning)' },
|
||||
{ value: 'gobuster', label: 'Gobuster (Directory Enumeration)' },
|
||||
{ value: 'sqlmap', label: 'SQLMap (SQL Injection Testing)' }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
run_scan: {
|
||||
title: 'Run Security Scan',
|
||||
steps: [
|
||||
{
|
||||
number: 1,
|
||||
title: 'Select Scan Tool',
|
||||
fields: [
|
||||
{ name: 'tool', label: 'Security Tool', type: 'select', required: true, options: [
|
||||
{ value: 'nmap', label: 'Nmap - Network Scanner' },
|
||||
{ value: 'nikto', label: 'Nikto - Web Server Scanner' },
|
||||
{ value: 'gobuster', label: 'Gobuster - Directory/File Discovery' },
|
||||
{ value: 'sqlmap', label: 'SQLMap - SQL Injection' },
|
||||
{ value: 'whatweb', label: 'WhatWeb - Technology Detection' }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: 'Specify Target',
|
||||
fields: [
|
||||
{ name: 'target', label: 'Target', type: 'text', required: true, placeholder: '192.168.1.0/24 or example.com' },
|
||||
{ name: 'ports', label: 'Ports (optional)', type: 'text', placeholder: '80,443,8080 or 1-1000' }
|
||||
]
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: 'Scan Options',
|
||||
fields: [
|
||||
{ name: 'scan_type', label: 'Scan Type', type: 'select', required: true, options: [
|
||||
{ value: 'quick', label: 'Quick Scan (Fast, common ports)' },
|
||||
{ value: 'full', label: 'Full Scan (Comprehensive, slower)' },
|
||||
{ value: 'stealth', label: 'Stealth Scan (Slow, harder to detect)' },
|
||||
{ value: 'vuln', label: 'Vulnerability Scan (Checks for known vulns)' }
|
||||
]},
|
||||
{ name: 'timeout', label: 'Timeout (seconds)', type: 'number', placeholder: '300' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
first_time_setup: {
|
||||
title: 'Welcome to StrikePackageGPT',
|
||||
steps: [
|
||||
{
|
||||
number: 1,
|
||||
title: 'Welcome',
|
||||
fields: [
|
||||
{ name: 'user_name', label: 'Your Name', type: 'text', placeholder: 'John Doe' },
|
||||
{ name: 'skill_level', label: 'Security Testing Experience', type: 'select', required: true, options: [
|
||||
{ value: 'beginner', label: 'Beginner - Learning the basics' },
|
||||
{ value: 'intermediate', label: 'Intermediate - Some experience' },
|
||||
{ value: 'advanced', label: 'Advanced - Professional pentester' }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: 'Configure LLM Provider',
|
||||
fields: [
|
||||
{ name: 'llm_provider', label: 'LLM Provider', type: 'select', required: true, options: [
|
||||
{ value: 'ollama', label: 'Ollama (Local, Free)' },
|
||||
{ value: 'openai', label: 'OpenAI (Cloud, Requires API Key)' },
|
||||
{ value: 'anthropic', label: 'Anthropic Claude (Cloud, Requires API Key)' }
|
||||
]},
|
||||
{ name: 'api_key', label: 'API Key (if using cloud provider)', type: 'password', placeholder: 'sk-...' }
|
||||
]
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: 'Review and Finish',
|
||||
fields: []
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const config = wizardConfigs[wizardType] || wizardConfigs.first_time_setup;
|
||||
const totalSteps = config.steps.length;
|
||||
const currentStepConfig = config.steps[currentStep - 1];
|
||||
|
||||
useEffect(() => {
|
||||
fetchStepHelp();
|
||||
}, [currentStep]);
|
||||
|
||||
const fetchStepHelp = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/wizard/help?type=${wizardType}&step=${currentStep}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setStepHelp(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch step help:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFieldChange = (fieldName, value) => {
|
||||
setFormData(prev => ({ ...prev, [fieldName]: value }));
|
||||
};
|
||||
|
||||
const validateCurrentStep = () => {
|
||||
const requiredFields = currentStepConfig.fields.filter(f => f.required);
|
||||
for (const field of requiredFields) {
|
||||
if (!formData[field.name]) {
|
||||
setError(`${field.label} is required`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setError(null);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (!validateCurrentStep()) return;
|
||||
|
||||
if (currentStep < totalSteps) {
|
||||
setCurrentStep(prev => prev + 1);
|
||||
} else {
|
||||
handleComplete();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep(prev => prev - 1);
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = async () => {
|
||||
if (!validateCurrentStep()) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
if (onComplete) {
|
||||
await onComplete(formData);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to complete wizard: ' + err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderField = (field) => {
|
||||
const commonStyle = {
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
};
|
||||
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
case 'password':
|
||||
case 'number':
|
||||
return (
|
||||
<input
|
||||
type={field.type}
|
||||
value={formData[field.name] || ''}
|
||||
onChange={(e) => handleFieldChange(field.name, e.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
style={commonStyle}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'textarea':
|
||||
return (
|
||||
<textarea
|
||||
value={formData[field.name] || ''}
|
||||
onChange={(e) => handleFieldChange(field.name, e.target.value)}
|
||||
placeholder={field.placeholder}
|
||||
rows={4}
|
||||
style={{ ...commonStyle, resize: 'vertical' }}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'select':
|
||||
return (
|
||||
<select
|
||||
value={formData[field.name] || ''}
|
||||
onChange={(e) => handleFieldChange(field.name, e.target.value)}
|
||||
style={commonStyle}
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
{field.options?.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
case 'multiselect':
|
||||
const selectedValues = formData[field.name] || [];
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{field.options?.map(opt => (
|
||||
<label key={opt.value} style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedValues.includes(opt.value)}
|
||||
onChange={(e) => {
|
||||
const newValues = e.target.checked
|
||||
? [...selectedValues, opt.value]
|
||||
: selectedValues.filter(v => v !== opt.value);
|
||||
handleFieldChange(field.name, newValues);
|
||||
}}
|
||||
/>
|
||||
<span>{opt.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
width: '90%',
|
||||
maxWidth: '700px',
|
||||
maxHeight: '90vh',
|
||||
overflow: 'auto',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)'
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
borderBottom: '2px solid #3498DB',
|
||||
backgroundColor: '#f8f9fa'
|
||||
}}>
|
||||
<h2 style={{ margin: '0 0 10px 0', color: '#2C3E50' }}>{config.title}</h2>
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div style={{ display: 'flex', gap: '5px', marginTop: '15px' }}>
|
||||
{config.steps.map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: '4px',
|
||||
backgroundColor: index + 1 <= currentStep ? '#3498DB' : '#ddd',
|
||||
borderRadius: '2px',
|
||||
transition: 'background-color 0.3s'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: '8px', fontSize: '14px', color: '#666' }}>
|
||||
Step {currentStep} of {totalSteps}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
<div style={{ padding: '30px' }}>
|
||||
<h3 style={{ margin: '0 0 20px 0', color: '#34495E' }}>
|
||||
{currentStepConfig.title}
|
||||
</h3>
|
||||
|
||||
{/* Help section */}
|
||||
{stepHelp && (
|
||||
<div style={{
|
||||
padding: '15px',
|
||||
backgroundColor: '#E8F4F8',
|
||||
borderRadius: '6px',
|
||||
marginBottom: '20px',
|
||||
borderLeft: '4px solid #3498DB'
|
||||
}}>
|
||||
{stepHelp.description && (
|
||||
<p style={{ margin: '0 0 10px 0' }}>{stepHelp.description}</p>
|
||||
)}
|
||||
{stepHelp.tips && stepHelp.tips.length > 0 && (
|
||||
<div>
|
||||
<strong style={{ fontSize: '14px' }}>💡 Tips:</strong>
|
||||
<ul style={{ margin: '5px 0 0 0', paddingLeft: '20px', fontSize: '14px' }}>
|
||||
{stepHelp.tips.map((tip, i) => (
|
||||
<li key={i} style={{ margin: '5px 0' }}>{tip}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form fields */}
|
||||
{currentStepConfig.fields.length > 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
{currentStepConfig.fields.map(field => (
|
||||
<div key={field.name}>
|
||||
<label style={{
|
||||
display: 'block',
|
||||
marginBottom: '8px',
|
||||
fontWeight: '500',
|
||||
color: '#2C3E50'
|
||||
}}>
|
||||
{field.label}
|
||||
{field.required && <span style={{ color: '#E74C3C' }}> *</span>}
|
||||
</label>
|
||||
{renderField(field)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
|
||||
<h4>Review Your Settings</h4>
|
||||
<div style={{
|
||||
marginTop: '20px',
|
||||
textAlign: 'left',
|
||||
backgroundColor: '#f8f9fa',
|
||||
padding: '15px',
|
||||
borderRadius: '4px',
|
||||
maxHeight: '300px',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{Object.entries(formData).map(([key, value]) => (
|
||||
<div key={key} style={{ marginBottom: '10px' }}>
|
||||
<strong>{key}:</strong> {Array.isArray(value) ? value.join(', ') : value}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div style={{
|
||||
marginTop: '20px',
|
||||
padding: '12px',
|
||||
backgroundColor: '#FCE4E4',
|
||||
color: '#E74C3C',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
borderTop: '1px solid #ddd',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: '#f8f9fa'
|
||||
}}>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
border: '1px solid #95A5A6',
|
||||
backgroundColor: 'white',
|
||||
color: '#666',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
{currentStep > 1 && (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
border: '1px solid #3498DB',
|
||||
backgroundColor: 'white',
|
||||
color: '#3498DB',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleNext}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
backgroundColor: loading ? '#95A5A6' : '#3498DB',
|
||||
color: 'white',
|
||||
borderRadius: '4px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Processing...' : currentStep === totalSteps ? 'Finish' : 'Next'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuidedWizard;
|
||||
424
services/dashboard/HelpChat.jsx
Normal file
@@ -0,0 +1,424 @@
|
||||
/**
|
||||
* HelpChat Component
|
||||
* Persistent side-panel chat with LLM-powered help
|
||||
* Context-aware and maintains conversation history
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const HelpChat = ({
|
||||
isOpen = false,
|
||||
onClose,
|
||||
currentPage = 'dashboard',
|
||||
context = {}
|
||||
}) => {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [sessionId] = useState(() => `session-${Date.now()}`);
|
||||
const messagesEndRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && messages.length === 0) {
|
||||
// Add welcome message
|
||||
setMessages([{
|
||||
role: 'assistant',
|
||||
content: `👋 Hi! I'm your AI assistant for StrikePackageGPT. I can help you with:
|
||||
|
||||
• Understanding security tools and commands
|
||||
• Interpreting scan results
|
||||
• Writing nmap, nikto, and other tool commands
|
||||
• Navigating the platform
|
||||
• Security best practices
|
||||
|
||||
What would you like help with?`,
|
||||
timestamp: new Date()
|
||||
}]);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!inputText.trim() || isLoading) return;
|
||||
|
||||
const userMessage = {
|
||||
role: 'user',
|
||||
content: inputText,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInputText('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Build context string
|
||||
const contextString = `User is on ${currentPage} page. ${JSON.stringify(context)}`;
|
||||
|
||||
const response = await fetch('/api/llm/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: inputText,
|
||||
session_id: sessionId,
|
||||
context: contextString
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get response');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const assistantMessage = {
|
||||
role: 'assistant',
|
||||
content: data.message || data.content || 'I apologize, I had trouble processing that request.',
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: '❌ Sorry, I encountered an error. Please try again.',
|
||||
timestamp: new Date(),
|
||||
isError: true
|
||||
}]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
// Could show a toast notification here
|
||||
console.log('Copied to clipboard');
|
||||
});
|
||||
};
|
||||
|
||||
const clearChat = () => {
|
||||
if (window.confirm('Clear all chat history?')) {
|
||||
setMessages([{
|
||||
role: 'assistant',
|
||||
content: 'Chat history cleared. How can I help you?',
|
||||
timestamp: new Date()
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
const renderMessage = (message, index) => {
|
||||
const isUser = message.role === 'user';
|
||||
const isError = message.isError;
|
||||
|
||||
// Check if message contains code blocks
|
||||
const hasCode = message.content.includes('```');
|
||||
let renderedContent;
|
||||
|
||||
if (hasCode) {
|
||||
// Simple code block rendering
|
||||
const parts = message.content.split(/(```[\s\S]*?```)/g);
|
||||
renderedContent = parts.map((part, i) => {
|
||||
if (part.startsWith('```')) {
|
||||
const code = part.slice(3, -3).trim();
|
||||
const [lang, ...codeLines] = code.split('\n');
|
||||
const codeText = codeLines.join('\n');
|
||||
|
||||
return (
|
||||
<div key={i} style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '10px',
|
||||
borderRadius: '4px',
|
||||
margin: '10px 0',
|
||||
position: 'relative',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '13px',
|
||||
overflowX: 'auto'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: '#666',
|
||||
marginBottom: '5px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<span>{lang || 'code'}</span>
|
||||
<button
|
||||
onClick={() => copyToClipboard(codeText)}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
fontSize: '11px',
|
||||
border: 'none',
|
||||
backgroundColor: '#ddd',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
📋 Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
|
||||
<code>{codeText}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div key={i} style={{ whiteSpace: 'pre-wrap' }}>{part}</div>;
|
||||
});
|
||||
} else {
|
||||
renderedContent = <div style={{ whiteSpace: 'pre-wrap' }}>{message.content}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: isUser ? 'flex-end' : 'flex-start',
|
||||
marginBottom: '15px'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '80%',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: isError ? '#FCE4E4' : isUser ? '#3498DB' : '#ECF0F1',
|
||||
color: isError ? '#E74C3C' : isUser ? 'white' : '#2C3E50',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
boxShadow: '0 2px 5px rgba(0,0,0,0.1)'
|
||||
}}
|
||||
>
|
||||
{renderedContent}
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: isUser ? 'rgba(255,255,255,0.7)' : '#95A5A6',
|
||||
marginTop: '5px',
|
||||
textAlign: 'right'
|
||||
}}
|
||||
>
|
||||
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const quickActions = [
|
||||
{ label: '📝 Write nmap command', prompt: 'How do I write an nmap command to scan a network?' },
|
||||
{ label: '🔍 Interpret results', prompt: 'Help me understand these scan results' },
|
||||
{ label: '🛠️ Use sqlmap', prompt: 'How do I use sqlmap to test for SQL injection?' },
|
||||
{ label: '📊 Generate report', prompt: 'How do I generate a security assessment report?' }
|
||||
];
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '400px',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '-4px 0 15px rgba(0,0,0,0.1)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
zIndex: 9998
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
padding: '20px',
|
||||
backgroundColor: '#3498DB',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderBottom: '2px solid #2980B9'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h3 style={{ margin: '0 0 5px 0', fontSize: '18px' }}>💬 AI Assistant</h3>
|
||||
<div style={{ fontSize: '12px', opacity: 0.9 }}>
|
||||
Ask me anything about security testing
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
onClick={clearChat}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '18px',
|
||||
padding: '5px'
|
||||
}}
|
||||
title="Clear chat"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '24px',
|
||||
padding: '0'
|
||||
}}
|
||||
title="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages area */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
padding: '20px',
|
||||
backgroundColor: '#FAFAFA'
|
||||
}}
|
||||
>
|
||||
{messages.map((message, index) => renderMessage(message, index))}
|
||||
|
||||
{isLoading && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', color: '#95A5A6' }}>
|
||||
<div>⏳</div>
|
||||
<div>Thinking...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Quick actions */}
|
||||
{messages.length <= 1 && (
|
||||
<div
|
||||
style={{
|
||||
padding: '15px',
|
||||
backgroundColor: '#F8F9FA',
|
||||
borderTop: '1px solid #ddd'
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginBottom: '10px' }}>
|
||||
Quick actions:
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{quickActions.map((action, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => {
|
||||
setInputText(action.prompt);
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
fontSize: '12px',
|
||||
border: '1px solid #ddd',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '16px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.backgroundColor = '#E8F4F8';
|
||||
e.target.style.borderColor = '#3498DB';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.backgroundColor = 'white';
|
||||
e.target.style.borderColor = '#ddd';
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input area */}
|
||||
<div
|
||||
style={{
|
||||
padding: '15px',
|
||||
borderTop: '2px solid #ECF0F1',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Ask a question... (Enter to send)"
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
resize: 'none',
|
||||
minHeight: '60px',
|
||||
maxHeight: '120px',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!inputText.trim() || isLoading}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
backgroundColor: !inputText.trim() || isLoading ? '#95A5A6' : '#3498DB',
|
||||
color: 'white',
|
||||
borderRadius: '8px',
|
||||
cursor: !inputText.trim() || isLoading ? 'not-allowed' : 'pointer',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500'
|
||||
}}
|
||||
>
|
||||
{isLoading ? '⏳' : '📤'}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: '#95A5A6', marginTop: '8px' }}>
|
||||
Shift+Enter for new line
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpChat;
|
||||
322
services/dashboard/NetworkMap.jsx
Normal file
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* NetworkMap Component
|
||||
* Interactive network graph visualization using Cytoscape.js
|
||||
* Displays discovered hosts from nmap scans with OS/device icons
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const NetworkMap = ({ scanId, onNodeClick }) => {
|
||||
const [hosts, setHosts] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filterText, setFilterText] = useState('');
|
||||
const cyRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (scanId) {
|
||||
fetchHostData(scanId);
|
||||
}
|
||||
}, [scanId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hosts.length > 0 && containerRef.current) {
|
||||
initializeNetwork();
|
||||
}
|
||||
}, [hosts]);
|
||||
|
||||
const fetchHostData = async (scanId) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/nmap/hosts?scan_id=${scanId}`);
|
||||
const data = await response.json();
|
||||
setHosts(data.hosts || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching host data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const initializeNetwork = () => {
|
||||
// NOTE: This component is a template for network visualization.
|
||||
// To use it, you must:
|
||||
// 1. Install cytoscape: npm install cytoscape
|
||||
// 2. Uncomment the code below and add the import at the top
|
||||
// 3. Build your React application with a bundler (webpack, vite, etc.)
|
||||
//
|
||||
// For a simpler integration without React build system, see INTEGRATION_EXAMPLE.md
|
||||
|
||||
// Example initialization (requires actual cytoscape import)
|
||||
/*
|
||||
import cytoscape from 'cytoscape';
|
||||
|
||||
const cy = cytoscape({
|
||||
container: containerRef.current,
|
||||
elements: buildGraphElements(hosts),
|
||||
style: getNetworkStyle(),
|
||||
layout: {
|
||||
name: 'cose',
|
||||
idealEdgeLength: 100,
|
||||
nodeOverlap: 20,
|
||||
refresh: 20,
|
||||
fit: true,
|
||||
padding: 30,
|
||||
randomize: false,
|
||||
componentSpacing: 100,
|
||||
nodeRepulsion: 400000,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 5,
|
||||
gravity: 80,
|
||||
numIter: 1000,
|
||||
initialTemp: 200,
|
||||
coolingFactor: 0.95,
|
||||
minTemp: 1.0
|
||||
}
|
||||
});
|
||||
|
||||
cy.on('tap', 'node', (evt) => {
|
||||
const node = evt.target;
|
||||
const hostData = node.data();
|
||||
if (onNodeClick) {
|
||||
onNodeClick(hostData);
|
||||
}
|
||||
});
|
||||
|
||||
cyRef.current = cy;
|
||||
*/
|
||||
};
|
||||
|
||||
const buildGraphElements = (hosts) => {
|
||||
const elements = [];
|
||||
|
||||
// Add nodes for each host
|
||||
hosts.forEach((host, index) => {
|
||||
elements.push({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: `host-${index}`,
|
||||
label: host.hostname || host.ip,
|
||||
...host,
|
||||
icon: getIconForHost(host)
|
||||
},
|
||||
classes: getNodeClass(host)
|
||||
});
|
||||
});
|
||||
|
||||
// Add edges (connections) - could be inferred from network topology
|
||||
// For now, connect hosts in same subnet
|
||||
const subnets = groupBySubnet(hosts);
|
||||
Object.values(subnets).forEach(subnetHosts => {
|
||||
if (subnetHosts.length > 1) {
|
||||
for (let i = 0; i < subnetHosts.length - 1; i++) {
|
||||
elements.push({
|
||||
group: 'edges',
|
||||
data: {
|
||||
id: `edge-${subnetHosts[i].ip}-${subnetHosts[i + 1].ip}`,
|
||||
source: `host-${hosts.indexOf(subnetHosts[i])}`,
|
||||
target: `host-${hosts.indexOf(subnetHosts[i + 1])}`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
const getIconForHost = (host) => {
|
||||
const osType = (host.os_type || '').toLowerCase();
|
||||
const deviceType = (host.device_type || '').toLowerCase();
|
||||
|
||||
if (deviceType.includes('server')) return '/static/server.svg';
|
||||
if (deviceType.includes('network') || deviceType.includes('router') || deviceType.includes('switch')) {
|
||||
return '/static/network.svg';
|
||||
}
|
||||
if (deviceType.includes('workstation')) return '/static/workstation.svg';
|
||||
|
||||
if (osType.includes('windows')) return '/static/windows.svg';
|
||||
if (osType.includes('linux') || osType.includes('unix')) return '/static/linux.svg';
|
||||
if (osType.includes('mac') || osType.includes('darwin')) return '/static/mac.svg';
|
||||
|
||||
return '/static/unknown.svg';
|
||||
};
|
||||
|
||||
const getNodeClass = (host) => {
|
||||
const deviceType = (host.device_type || '').toLowerCase();
|
||||
if (deviceType.includes('server')) return 'node-server';
|
||||
if (deviceType.includes('network')) return 'node-network';
|
||||
if (deviceType.includes('workstation')) return 'node-workstation';
|
||||
return 'node-unknown';
|
||||
};
|
||||
|
||||
const groupBySubnet = (hosts) => {
|
||||
const subnets = {};
|
||||
hosts.forEach(host => {
|
||||
const subnet = host.ip.split('.').slice(0, 3).join('.');
|
||||
if (!subnets[subnet]) {
|
||||
subnets[subnet] = [];
|
||||
}
|
||||
subnets[subnet].push(host);
|
||||
});
|
||||
return subnets;
|
||||
};
|
||||
|
||||
const getNetworkStyle = () => {
|
||||
return [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': '#4A90E2',
|
||||
'label': 'data(label)',
|
||||
'text-valign': 'bottom',
|
||||
'text-halign': 'center',
|
||||
'font-size': '12px',
|
||||
'color': '#333',
|
||||
'text-margin-y': 5,
|
||||
'width': 50,
|
||||
'height': 50,
|
||||
'background-image': 'data(icon)',
|
||||
'background-fit': 'contain'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: '.node-server',
|
||||
style: {
|
||||
'background-color': '#4A90E2'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: '.node-network',
|
||||
style: {
|
||||
'background-color': '#16A085'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: '.node-workstation',
|
||||
style: {
|
||||
'background-color': '#5DADE2'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': 2,
|
||||
'line-color': '#95A5A6',
|
||||
'curve-style': 'bezier'
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'node:selected',
|
||||
style: {
|
||||
'border-width': 3,
|
||||
'border-color': '#E74C3C'
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
const exportToPNG = () => {
|
||||
if (cyRef.current) {
|
||||
const png = cyRef.current.png({ scale: 2, full: true });
|
||||
const link = document.createElement('a');
|
||||
link.href = png;
|
||||
link.download = `network-map-${Date.now()}.png`;
|
||||
link.click();
|
||||
}
|
||||
};
|
||||
|
||||
const exportToCSV = () => {
|
||||
const csvContent = [
|
||||
['IP', 'Hostname', 'OS Type', 'Device Type', 'MAC', 'Vendor', 'Open Ports'].join(','),
|
||||
...hosts.map(host => [
|
||||
host.ip,
|
||||
host.hostname || '',
|
||||
host.os_type || '',
|
||||
host.device_type || '',
|
||||
host.mac || '',
|
||||
host.vendor || '',
|
||||
(host.ports || []).map(p => p.port).join(';')
|
||||
].join(','))
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `network-hosts-${Date.now()}.csv`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const filteredHosts = hosts.filter(host => {
|
||||
if (!filterText) return true;
|
||||
const searchLower = filterText.toLowerCase();
|
||||
return (
|
||||
host.ip.includes(searchLower) ||
|
||||
(host.hostname || '').toLowerCase().includes(searchLower) ||
|
||||
(host.os_type || '').toLowerCase().includes(searchLower) ||
|
||||
(host.device_type || '').toLowerCase().includes(searchLower)
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="network-map-container" style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div className="network-map-toolbar" style={{ padding: '10px', borderBottom: '1px solid #ddd', display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter hosts (IP, hostname, OS, device type)..."
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
style={{ flex: 1, padding: '8px', borderRadius: '4px', border: '1px solid #ccc' }}
|
||||
/>
|
||||
<button onClick={exportToPNG} style={{ padding: '8px 16px', cursor: 'pointer', borderRadius: '4px' }}>
|
||||
Export PNG
|
||||
</button>
|
||||
<button onClick={exportToCSV} style={{ padding: '8px 16px', cursor: 'pointer', borderRadius: '4px' }}>
|
||||
Export CSV
|
||||
</button>
|
||||
<span style={{ color: '#666' }}>
|
||||
{filteredHosts.length} host{filteredHosts.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="network-map-canvas"
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: '#f5f5f5',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{loading && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div>Loading network map...</div>
|
||||
</div>
|
||||
)}
|
||||
{!loading && hosts.length === 0 && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
textAlign: 'center',
|
||||
color: '#666'
|
||||
}}>
|
||||
<div>No hosts discovered yet</div>
|
||||
<div style={{ fontSize: '14px', marginTop: '10px' }}>Run a network scan to populate the map</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkMap;
|
||||
356
services/dashboard/VoiceControls.jsx
Normal file
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* VoiceControls Component
|
||||
* Microphone button with hotkey support for voice commands
|
||||
* Visual feedback for listening, processing, and speaking states
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const VoiceControls = ({ onCommand, hotkey = ' ' }) => {
|
||||
const [state, setState] = useState('idle'); // idle, listening, processing, speaking
|
||||
const [transcript, setTranscript] = useState('');
|
||||
const [error, setError] = useState(null);
|
||||
const [permissionGranted, setPermissionGranted] = useState(false);
|
||||
const mediaRecorderRef = useRef(null);
|
||||
const audioChunksRef = useRef([]);
|
||||
const hotkeyPressedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if browser supports MediaRecorder
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
setError('Voice control not supported in this browser');
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: We request permission on mount for better UX.
|
||||
// Alternative: Request only on first use by removing this and letting
|
||||
// startListening() handle the permission request
|
||||
requestMicrophonePermission();
|
||||
|
||||
// Setup hotkey listener
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === hotkey && !hotkeyPressedRef.current && state === 'idle') {
|
||||
hotkeyPressedRef.current = true;
|
||||
startListening();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (e) => {
|
||||
if (e.key === hotkey && hotkeyPressedRef.current) {
|
||||
hotkeyPressedRef.current = false;
|
||||
if (state === 'listening') {
|
||||
stopListening();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
if (mediaRecorderRef.current && state === 'listening') {
|
||||
mediaRecorderRef.current.stop();
|
||||
}
|
||||
};
|
||||
}, [hotkey, state]);
|
||||
|
||||
const requestMicrophonePermission = async () => {
|
||||
try {
|
||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
setPermissionGranted(true);
|
||||
} catch (err) {
|
||||
setError('Microphone permission denied');
|
||||
setPermissionGranted(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startListening = async () => {
|
||||
if (!permissionGranted) {
|
||||
await requestMicrophonePermission();
|
||||
if (!permissionGranted) return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorderRef.current = mediaRecorder;
|
||||
audioChunksRef.current = [];
|
||||
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
audioChunksRef.current.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = async () => {
|
||||
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
|
||||
await processAudio(audioBlob);
|
||||
|
||||
// Stop all tracks
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
setState('listening');
|
||||
setTranscript('');
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('Error starting recording:', err);
|
||||
setError('Failed to start recording: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const stopListening = () => {
|
||||
if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') {
|
||||
mediaRecorderRef.current.stop();
|
||||
}
|
||||
};
|
||||
|
||||
const processAudio = async (audioBlob) => {
|
||||
setState('processing');
|
||||
|
||||
try {
|
||||
// Send audio to backend for transcription
|
||||
const formData = new FormData();
|
||||
formData.append('audio', audioBlob, 'recording.webm');
|
||||
|
||||
const response = await fetch('/api/voice/transcribe', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Transcription failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const transcribedText = data.text || '';
|
||||
|
||||
setTranscript(transcribedText);
|
||||
|
||||
if (transcribedText) {
|
||||
// Parse and route the voice command
|
||||
await routeCommand(transcribedText);
|
||||
} else {
|
||||
setError('No speech detected');
|
||||
setState('idle');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error processing audio:', err);
|
||||
setError('Failed to process audio: ' + err.message);
|
||||
setState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
const routeCommand = async (text) => {
|
||||
try {
|
||||
const response = await fetch('/api/voice/command', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Command routing failed');
|
||||
}
|
||||
|
||||
const commandResult = await response.json();
|
||||
|
||||
// Call parent callback with command result
|
||||
if (onCommand) {
|
||||
onCommand(commandResult);
|
||||
}
|
||||
|
||||
// Check if TTS response is available
|
||||
if (commandResult.speak_response) {
|
||||
await speakResponse(commandResult.speak_response);
|
||||
} else {
|
||||
setState('idle');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error routing command:', err);
|
||||
setError('Failed to execute command: ' + err.message);
|
||||
setState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
const speakResponse = async (text) => {
|
||||
setState('speaking');
|
||||
|
||||
try {
|
||||
// Try to get TTS audio from backend
|
||||
const response = await fetch('/api/voice/speak', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const audioBlob = await response.blob();
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
const audio = new Audio(audioUrl);
|
||||
|
||||
audio.onended = () => {
|
||||
setState('idle');
|
||||
URL.revokeObjectURL(audioUrl);
|
||||
};
|
||||
|
||||
audio.play();
|
||||
} else {
|
||||
// Fallback to browser TTS
|
||||
if ('speechSynthesis' in window) {
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.onend = () => setState('idle');
|
||||
window.speechSynthesis.speak(utterance);
|
||||
} else {
|
||||
setState('idle');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error speaking response:', err);
|
||||
setState('idle');
|
||||
}
|
||||
};
|
||||
|
||||
const getStateColor = () => {
|
||||
switch (state) {
|
||||
case 'listening': return '#27AE60';
|
||||
case 'processing': return '#F39C12';
|
||||
case 'speaking': return '#3498DB';
|
||||
default: return '#95A5A6';
|
||||
}
|
||||
};
|
||||
|
||||
const getStateIcon = () => {
|
||||
switch (state) {
|
||||
case 'listening': return '🎤';
|
||||
case 'processing': return '⏳';
|
||||
case 'speaking': return '🔊';
|
||||
default: return '🎙️';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="voice-controls"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
right: '20px',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-end',
|
||||
gap: '10px'
|
||||
}}
|
||||
>
|
||||
{/* Transcript display */}
|
||||
{transcript && (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
padding: '10px 15px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
||||
maxWidth: '300px',
|
||||
fontSize: '14px',
|
||||
color: '#333'
|
||||
}}
|
||||
>
|
||||
<strong>You said:</strong> {transcript}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error display */}
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#E74C3C',
|
||||
color: 'white',
|
||||
padding: '10px 15px',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '300px',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mic button */}
|
||||
<button
|
||||
onClick={state === 'idle' ? startListening : stopListening}
|
||||
disabled={state === 'processing' || state === 'speaking'}
|
||||
style={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
backgroundColor: getStateColor(),
|
||||
color: 'white',
|
||||
fontSize: '24px',
|
||||
cursor: state === 'idle' || state === 'listening' ? 'pointer' : 'not-allowed',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.3s ease',
|
||||
transform: state === 'listening' ? 'scale(1.1)' : 'scale(1)',
|
||||
opacity: state === 'processing' || state === 'speaking' ? 0.7 : 1
|
||||
}}
|
||||
title={`Voice command (hold ${hotkey === ' ' ? 'Space' : hotkey})`}
|
||||
>
|
||||
{getStateIcon()}
|
||||
</button>
|
||||
|
||||
{/* Pulsing animation for listening state */}
|
||||
{state === 'listening' && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
right: '0',
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
border: '3px solid #27AE60',
|
||||
animation: 'pulse 1.5s infinite',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Hotkey hint */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
Hold {hotkey === ' ' ? 'Space' : hotkey} to talk
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.3);
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.6);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VoiceControls;
|
||||
@@ -43,7 +43,7 @@ class ChatMessage(BaseModel):
|
||||
message: str
|
||||
session_id: Optional[str] = None
|
||||
provider: str = "ollama"
|
||||
model: str = "llama3.2"
|
||||
model: str = "llama3.1:latest"
|
||||
context: Optional[str] = None
|
||||
|
||||
|
||||
@@ -51,14 +51,14 @@ class PhaseChatMessage(BaseModel):
|
||||
message: str
|
||||
phase: str
|
||||
provider: str = "ollama"
|
||||
model: str = "llama3.2"
|
||||
model: str = "llama3.1:latest"
|
||||
findings: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AttackChainRequest(BaseModel):
|
||||
findings: List[Dict[str, Any]]
|
||||
provider: str = "ollama"
|
||||
model: str = "llama3.2"
|
||||
model: str = "llama3.1:latest"
|
||||
|
||||
|
||||
class CommandRequest(BaseModel):
|
||||
@@ -79,6 +79,18 @@ class NetworkScanRequest(BaseModel):
|
||||
scan_type: str = "os" # ping, quick, os, full
|
||||
|
||||
|
||||
class TerminalScanRecord(BaseModel):
|
||||
tool: str
|
||||
target: str
|
||||
command: str
|
||||
output: str
|
||||
source: str = "terminal"
|
||||
|
||||
|
||||
# Local storage for terminal scans
|
||||
terminal_scans: List[Dict[str, Any]] = []
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
@@ -312,20 +324,73 @@ async def get_scan_result(scan_id: str):
|
||||
|
||||
@app.get("/api/scans")
|
||||
async def list_scans():
|
||||
"""List all scans"""
|
||||
"""List all scans (from hackgpt-api and terminal)"""
|
||||
all_scans = list(terminal_scans) # Start with terminal scans
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{HACKGPT_API_URL}/scans", timeout=10.0)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
raise HTTPException(status_code=response.status_code, detail=response.text)
|
||||
api_scans = response.json()
|
||||
all_scans.extend(api_scans)
|
||||
except httpx.ConnectError:
|
||||
raise HTTPException(status_code=503, detail="HackGPT API not available")
|
||||
pass # Return terminal scans even if API is down
|
||||
return all_scans
|
||||
|
||||
|
||||
@app.post("/api/scans/terminal")
|
||||
async def record_terminal_scan(scan: TerminalScanRecord):
|
||||
"""Record a scan run from the terminal"""
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
scan_id = str(uuid.uuid4())
|
||||
|
||||
# Parse target from command if not provided
|
||||
target = scan.target
|
||||
if target == "unknown" and scan.command:
|
||||
parts = scan.command.split()
|
||||
# Try to find target (usually last non-flag argument)
|
||||
for part in reversed(parts):
|
||||
if not part.startswith('-') and '/' not in part:
|
||||
target = part
|
||||
break
|
||||
|
||||
scan_record = {
|
||||
"scan_id": scan_id,
|
||||
"tool": scan.tool,
|
||||
"target": target,
|
||||
"scan_type": "terminal",
|
||||
"command": scan.command,
|
||||
"status": "completed",
|
||||
"started_at": datetime.utcnow().isoformat(),
|
||||
"completed_at": datetime.utcnow().isoformat(),
|
||||
"result": {
|
||||
"stdout": scan.output,
|
||||
"stderr": "",
|
||||
"exit_code": 0
|
||||
},
|
||||
"source": scan.source,
|
||||
"parsed": None
|
||||
}
|
||||
|
||||
# Parse the output for common tools
|
||||
if scan.tool == "nmap":
|
||||
scan_record["parsed"] = parse_nmap_normal(scan.output)
|
||||
|
||||
terminal_scans.append(scan_record)
|
||||
|
||||
# Keep only last 100 terminal scans
|
||||
if len(terminal_scans) > 100:
|
||||
terminal_scans.pop(0)
|
||||
|
||||
return {"status": "recorded", "scan_id": scan_id}
|
||||
|
||||
|
||||
@app.delete("/api/scans/clear")
|
||||
async def clear_scans():
|
||||
"""Clear all scan history"""
|
||||
global terminal_scans
|
||||
terminal_scans = [] # Clear local terminal scans
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.delete(f"{HACKGPT_API_URL}/scans/clear", timeout=10.0)
|
||||
@@ -766,6 +831,301 @@ async def get_network_hosts():
|
||||
return {"hosts": network_hosts}
|
||||
|
||||
|
||||
class HostData(BaseModel):
|
||||
ip: str
|
||||
hostname: Optional[str] = None
|
||||
mac: Optional[str] = None
|
||||
vendor: Optional[str] = None
|
||||
os: Optional[str] = None
|
||||
os_accuracy: Optional[int] = None
|
||||
device_type: Optional[str] = None
|
||||
ports: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
status: str = "up"
|
||||
source: str = "terminal"
|
||||
|
||||
|
||||
class NmapResultData(BaseModel):
|
||||
"""Accept raw nmap output for parsing"""
|
||||
output: str
|
||||
source: str = "terminal"
|
||||
|
||||
|
||||
@app.post("/api/network/hosts")
|
||||
async def add_network_host(host: HostData):
|
||||
"""Add a single host to the network map (from terminal scans)"""
|
||||
global network_hosts
|
||||
|
||||
host_dict = host.dict()
|
||||
existing = next((h for h in network_hosts if h["ip"] == host.ip), None)
|
||||
if existing:
|
||||
existing.update(host_dict)
|
||||
else:
|
||||
network_hosts.append(host_dict)
|
||||
|
||||
return {"status": "added", "host": host_dict}
|
||||
|
||||
|
||||
@app.post("/api/network/hosts/bulk")
|
||||
async def add_network_hosts_bulk(hosts: List[HostData]):
|
||||
"""Add multiple hosts to the network map"""
|
||||
global network_hosts
|
||||
|
||||
added = 0
|
||||
updated = 0
|
||||
for host in hosts:
|
||||
host_dict = host.dict()
|
||||
existing = next((h for h in network_hosts if h["ip"] == host.ip), None)
|
||||
if existing:
|
||||
existing.update(host_dict)
|
||||
updated += 1
|
||||
else:
|
||||
network_hosts.append(host_dict)
|
||||
added += 1
|
||||
|
||||
return {"status": "success", "added": added, "updated": updated, "total": len(network_hosts)}
|
||||
|
||||
|
||||
@app.post("/api/network/nmap-results")
|
||||
async def add_nmap_results(data: NmapResultData):
|
||||
"""Parse and add nmap output to network map (supports both XML and grepable formats)"""
|
||||
global network_hosts
|
||||
|
||||
hosts = parse_nmap_xml(data.output)
|
||||
|
||||
# If XML parsing didn't work, try parsing grepable/normal output
|
||||
if not hosts:
|
||||
hosts = parse_nmap_normal(data.output)
|
||||
|
||||
added = 0
|
||||
updated = 0
|
||||
for host in hosts:
|
||||
host["source"] = data.source
|
||||
existing = next((h for h in network_hosts if h["ip"] == host["ip"]), None)
|
||||
if existing:
|
||||
existing.update(host)
|
||||
updated += 1
|
||||
else:
|
||||
network_hosts.append(host)
|
||||
added += 1
|
||||
|
||||
return {"status": "success", "added": added, "updated": updated, "hosts_parsed": len(hosts), "total": len(network_hosts)}
|
||||
|
||||
|
||||
def parse_nmap_normal(output: str) -> List[Dict[str, Any]]:
|
||||
"""Parse normal nmap output (non-XML)"""
|
||||
import re
|
||||
hosts = []
|
||||
current_host = None
|
||||
|
||||
for line in output.split('\n'):
|
||||
# Match "Nmap scan report for hostname (ip)" or "Nmap scan report for ip"
|
||||
report_match = re.match(r'Nmap scan report for (?:([^\s(]+)\s+\()?([0-9.]+)\)?', line)
|
||||
if report_match:
|
||||
if current_host:
|
||||
# Determine OS type before saving
|
||||
current_host["os_type"] = determine_os_type(
|
||||
current_host.get("os", ""),
|
||||
current_host.get("ports", []),
|
||||
current_host.get("mac", ""),
|
||||
current_host.get("vendor", "")
|
||||
)
|
||||
hosts.append(current_host)
|
||||
hostname = report_match.group(1) or ""
|
||||
ip = report_match.group(2)
|
||||
current_host = {
|
||||
"ip": ip,
|
||||
"hostname": hostname,
|
||||
"status": "up",
|
||||
"ports": [],
|
||||
"os": "",
|
||||
"os_type": "",
|
||||
"device_type": ""
|
||||
}
|
||||
continue
|
||||
|
||||
# Match "Host is up"
|
||||
if current_host and "Host is up" in line:
|
||||
current_host["status"] = "up"
|
||||
continue
|
||||
|
||||
# Match port lines like "80/tcp open http Apache httpd"
|
||||
port_match = re.match(r'(\d+)/(tcp|udp)\s+(\w+)\s+(\S+)(?:\s+(.*))?', line)
|
||||
if port_match and current_host:
|
||||
port_info = {
|
||||
"port": int(port_match.group(1)),
|
||||
"protocol": port_match.group(2),
|
||||
"state": port_match.group(3),
|
||||
"service": port_match.group(4),
|
||||
"version": (port_match.group(5) or "").strip()
|
||||
}
|
||||
current_host["ports"].append(port_info)
|
||||
continue
|
||||
|
||||
# Match MAC address
|
||||
mac_match = re.match(r'MAC Address:\s+([0-9A-F:]+)\s*(?:\((.+)\))?', line, re.IGNORECASE)
|
||||
if mac_match and current_host:
|
||||
current_host["mac"] = mac_match.group(1)
|
||||
current_host["vendor"] = mac_match.group(2) or ""
|
||||
continue
|
||||
|
||||
# Match OS detection - multiple patterns
|
||||
os_match = re.match(r'(?:OS details|Running|OS):\s+(.+)', line)
|
||||
if os_match and current_host:
|
||||
os_info = os_match.group(1).strip()
|
||||
if os_info and not current_host["os"]:
|
||||
current_host["os"] = os_info
|
||||
elif os_info:
|
||||
current_host["os"] += "; " + os_info
|
||||
continue
|
||||
|
||||
# Match smb-os-discovery OS line
|
||||
smb_os_match = re.match(r'\|\s+OS:\s+(.+)', line)
|
||||
if smb_os_match and current_host:
|
||||
os_info = smb_os_match.group(1).strip()
|
||||
if not current_host["os"]:
|
||||
current_host["os"] = os_info
|
||||
elif os_info not in current_host["os"]:
|
||||
current_host["os"] += "; " + os_info
|
||||
continue
|
||||
|
||||
# Match device type
|
||||
device_match = re.match(r'Device type:\s+(.+)', line)
|
||||
if device_match and current_host:
|
||||
current_host["device_type"] = device_match.group(1)
|
||||
continue
|
||||
|
||||
# Don't forget the last host
|
||||
if current_host:
|
||||
current_host["os_type"] = determine_os_type(
|
||||
current_host.get("os", ""),
|
||||
current_host.get("ports", []),
|
||||
current_host.get("mac", ""),
|
||||
current_host.get("vendor", "")
|
||||
)
|
||||
hosts.append(current_host)
|
||||
|
||||
return hosts
|
||||
|
||||
|
||||
def determine_os_type(os_string: str, ports: List[Dict], mac: str = "", vendor: str = "") -> str:
|
||||
"""Determine OS type from OS string, open ports, MAC address, and vendor"""
|
||||
os_lower = os_string.lower()
|
||||
vendor_lower = vendor.lower() if vendor else ""
|
||||
|
||||
# Check OS string for keywords
|
||||
if any(kw in os_lower for kw in ['windows', 'microsoft', 'win32', 'win64']):
|
||||
return 'windows'
|
||||
if any(kw in os_lower for kw in ['linux', 'ubuntu', 'debian', 'centos', 'fedora', 'redhat', 'kali']):
|
||||
return 'linux'
|
||||
if any(kw in os_lower for kw in ['mac os', 'macos', 'darwin', 'apple']):
|
||||
return 'macos'
|
||||
if any(kw in os_lower for kw in ['cisco', 'router', 'switch', 'juniper', 'mikrotik']):
|
||||
return 'router'
|
||||
if any(kw in os_lower for kw in ['unix', 'freebsd', 'openbsd', 'solaris']):
|
||||
return 'linux' # Close enough for icon purposes
|
||||
|
||||
# Check MAC address OUI for manufacturer hints
|
||||
mac_vendor = get_mac_vendor_hint(mac, vendor)
|
||||
if mac_vendor:
|
||||
return mac_vendor
|
||||
|
||||
# Infer from ports if OS string didn't help
|
||||
port_nums = [p.get('port') for p in ports if p.get('state') == 'open']
|
||||
services = [p.get('service', '').lower() for p in ports]
|
||||
versions = [p.get('version', '').lower() for p in ports]
|
||||
|
||||
# Windows indicators
|
||||
windows_ports = {135, 139, 445, 3389, 5985, 5986}
|
||||
if windows_ports & set(port_nums):
|
||||
# Check if it's actually Samba on Linux
|
||||
if any('samba' in v for v in versions):
|
||||
return 'linux'
|
||||
return 'windows'
|
||||
|
||||
# Check service versions for OS hints
|
||||
for version in versions:
|
||||
if 'windows' in version or 'microsoft' in version:
|
||||
return 'windows'
|
||||
if 'ubuntu' in version or 'debian' in version or 'linux' in version:
|
||||
return 'linux'
|
||||
|
||||
# Linux indicators
|
||||
if 22 in port_nums and any(s in services for s in ['ssh', 'openssh']):
|
||||
return 'linux'
|
||||
|
||||
# Router/network device indicators
|
||||
if any(s in services for s in ['telnet', 'snmp']) and 80 in port_nums:
|
||||
return 'router'
|
||||
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def get_mac_vendor_hint(mac: str, vendor: str = "") -> Optional[str]:
|
||||
"""Get OS type hint from MAC address OUI or vendor string"""
|
||||
if not mac and not vendor:
|
||||
return None
|
||||
|
||||
vendor_lower = vendor.lower() if vendor else ""
|
||||
|
||||
# Check vendor string first (nmap often provides this)
|
||||
# Apple devices
|
||||
if any(kw in vendor_lower for kw in ['apple', 'iphone', 'ipad', 'macbook']):
|
||||
return 'macos'
|
||||
|
||||
# Network equipment
|
||||
if any(kw in vendor_lower for kw in ['cisco', 'juniper', 'netgear', 'linksys', 'tp-link', 'ubiquiti', 'mikrotik', 'd-link', 'asus router', 'arris', 'netcomm']):
|
||||
return 'router'
|
||||
|
||||
# VM/Hypervisor (likely running any OS, but probably server/linux)
|
||||
if any(kw in vendor_lower for kw in ['vmware', 'virtualbox', 'hyper-v', 'microsoft hyper', 'xen', 'qemu', 'kvm']):
|
||||
return 'server'
|
||||
|
||||
# Raspberry Pi
|
||||
if 'raspberry' in vendor_lower:
|
||||
return 'linux'
|
||||
|
||||
# Known PC/laptop manufacturers (could be Windows or Linux)
|
||||
pc_vendors = ['dell', 'hewlett', 'lenovo', 'asus', 'acer', 'msi', 'gigabyte', 'intel', 'realtek', 'broadcom', 'qualcomm', 'mediatek']
|
||||
if any(kw in vendor_lower for kw in pc_vendors):
|
||||
# Most likely Windows for consumer PCs, but we can't be certain
|
||||
# Return None to let other detection methods decide
|
||||
return None
|
||||
|
||||
# MAC OUI lookup for common prefixes
|
||||
if mac:
|
||||
mac_upper = mac.upper().replace(':', '').replace('-', '')[:6]
|
||||
|
||||
# VMware
|
||||
if mac_upper.startswith(('005056', '000C29', '000569')):
|
||||
return 'server'
|
||||
|
||||
# Microsoft Hyper-V
|
||||
if mac_upper.startswith('00155D'):
|
||||
return 'server'
|
||||
|
||||
# Apple
|
||||
if mac_upper.startswith(('A4B197', '3C0754', '009027', 'ACDE48', 'F0B479', '70CD60', '00A040', '000A27', '000393', '001CB3', '001D4F', '001E52', '001F5B', '001FF3', '0021E9', '002241', '002312', '002332', '002436', '00254B', '0025BC', '002608', '00264A', '0026B0', '0026BB')):
|
||||
return 'macos'
|
||||
|
||||
# Raspberry Pi Foundation
|
||||
if mac_upper.startswith(('B827EB', 'DCA632', 'E45F01', 'DC2632')):
|
||||
return 'linux'
|
||||
|
||||
# Cisco
|
||||
if mac_upper.startswith(('001121', '00166D', '001819', '001832', '00186B', '00187D', '0018B9', '0018F3', '00195E', '001A2F', '001A6C', '001A6D', '001B2A', '001BD4', '001C01', '001C0E', '001CE6', '001E13', '001E49', '001EBD', '001F27', '001F6C', '001F9D', '00212F', '002155', '0021A0', '0021BE', '0021D7', '002216', '00223A', '002255', '00226B', '002275', '0023AB', '0023AC', '0023BE', '0023EA', '00240A', '002414', '002436', '00248C', '0024F7', '00250B', '002583', '0025B4', '0026CB', '0027DC', '002841', '0029C2', '002A10', '002A6A', '002CC8', '0030A3', '003080', '0030B6', '00400B', '004096', '005000', '005014', '00501E', '00503E', '005050', '00505F', '005073', '0050A2', '0050D1', '0050E2', '0050F0', '006009', '00602F', '006047', '006052', '00606D', '00607B', '006083', '00609E', '0060B0', '00908F', '0090A6', '009092')):
|
||||
return 'router'
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@app.delete("/api/network/hosts")
|
||||
async def clear_network_hosts():
|
||||
"""Clear all network hosts"""
|
||||
global network_hosts
|
||||
network_hosts = []
|
||||
return {"status": "cleared"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8080)
|
||||
9
services/dashboard/static/linux.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<circle cx="24" cy="24" r="22" fill="#000000"/>
|
||||
<ellipse cx="24" cy="20" rx="12" ry="14" fill="#FFFFFF"/>
|
||||
<ellipse cx="24" cy="28" rx="10" ry="8" fill="#FDB515"/>
|
||||
<circle cx="20" cy="18" r="2" fill="#000000"/>
|
||||
<circle cx="28" cy="18" r="2" fill="#000000"/>
|
||||
<path d="M 24 22 Q 22 24 24 24 Q 26 24 24 22" fill="none" stroke="#000000" stroke-width="1.5"/>
|
||||
<path d="M 16 26 Q 18 30 24 32 Q 30 30 32 26" fill="none" stroke="#000000" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 554 B |
8
services/dashboard/static/mac.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<circle cx="24" cy="24" r="22" fill="#A2AAAD"/>
|
||||
<path d="M 30 14 Q 28 10 24 10 Q 20 10 18 14 Q 16 18 18 22 Q 20 26 24 26 Q 28 26 30 22 Q 32 18 30 14 Z" fill="#FFFFFF"/>
|
||||
<circle cx="24" cy="16" r="4" fill="#A2AAAD"/>
|
||||
<path d="M 26 10 Q 28 8 30 10" stroke="#FFFFFF" stroke-width="2" fill="none"/>
|
||||
<rect x="22" y="26" width="4" height="8" fill="#FFFFFF"/>
|
||||
<path d="M 18 34 Q 20 36 24 36 Q 28 36 30 34 L 28 32 Q 26 33 24 33 Q 22 33 20 32 Z" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 557 B |
16
services/dashboard/static/network.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<rect width="48" height="48" fill="#16A085" rx="2"/>
|
||||
<rect x="6" y="18" width="36" height="12" rx="1" fill="#2C3E50"/>
|
||||
<circle cx="10" cy="24" r="1.5" fill="#27AE60"/>
|
||||
<circle cx="14" cy="24" r="1.5" fill="#27AE60"/>
|
||||
<circle cx="18" cy="24" r="1.5" fill="#27AE60"/>
|
||||
<circle cx="22" cy="24" r="1.5" fill="#27AE60"/>
|
||||
<circle cx="26" cy="24" r="1.5" fill="#F39C12"/>
|
||||
<circle cx="30" cy="24" r="1.5" fill="#95A5A6"/>
|
||||
<circle cx="34" cy="24" r="1.5" fill="#95A5A6"/>
|
||||
<circle cx="38" cy="24" r="1.5" fill="#95A5A6"/>
|
||||
<line x1="24" y1="10" x2="24" y2="18" stroke="#ECF0F1" stroke-width="2"/>
|
||||
<line x1="24" y1="30" x2="24" y2="38" stroke="#ECF0F1" stroke-width="2"/>
|
||||
<line x1="10" y1="24" x2="4" y2="24" stroke="#ECF0F1" stroke-width="2"/>
|
||||
<line x1="38" y1="24" x2="44" y2="24" stroke="#ECF0F1" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 925 B |
15
services/dashboard/static/server.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<rect width="48" height="48" fill="#4A90E2" rx="2"/>
|
||||
<rect x="6" y="6" width="36" height="10" rx="1" fill="#2C3E50"/>
|
||||
<rect x="6" y="19" width="36" height="10" rx="1" fill="#34495E"/>
|
||||
<rect x="6" y="32" width="36" height="10" rx="1" fill="#2C3E50"/>
|
||||
<circle cx="10" cy="11" r="1.5" fill="#27AE60"/>
|
||||
<circle cx="14" cy="11" r="1.5" fill="#F39C12"/>
|
||||
<circle cx="10" cy="24" r="1.5" fill="#27AE60"/>
|
||||
<circle cx="14" cy="24" r="1.5" fill="#27AE60"/>
|
||||
<circle cx="10" cy="37" r="1.5" fill="#E74C3C"/>
|
||||
<circle cx="14" cy="37" r="1.5" fill="#F39C12"/>
|
||||
<rect x="18" y="9" width="20" height="4" rx="0.5" fill="#7F8C8D"/>
|
||||
<rect x="18" y="22" width="20" height="4" rx="0.5" fill="#7F8C8D"/>
|
||||
<rect x="18" y="35" width="20" height="4" rx="0.5" fill="#7F8C8D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 864 B |
5
services/dashboard/static/unknown.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<circle cx="24" cy="24" r="22" fill="#95A5A6"/>
|
||||
<circle cx="24" cy="24" r="18" fill="#7F8C8D"/>
|
||||
<text x="24" y="32" font-size="24" font-weight="bold" fill="#ECF0F1" text-anchor="middle" font-family="Arial">?</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 312 B |
7
services/dashboard/static/windows.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<rect width="48" height="48" fill="#0078D4" rx="2"/>
|
||||
<rect x="6" y="6" width="17" height="17" fill="#FFFFFF"/>
|
||||
<rect x="25" y="6" width="17" height="17" fill="#FFFFFF"/>
|
||||
<rect x="6" y="25" width="17" height="17" fill="#FFFFFF"/>
|
||||
<rect x="25" y="25" width="17" height="17" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 390 B |
9
services/dashboard/static/workstation.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48">
|
||||
<rect width="48" height="48" fill="#5DADE2" rx="2"/>
|
||||
<rect x="8" y="8" width="32" height="22" rx="1" fill="#34495E"/>
|
||||
<rect x="10" y="10" width="28" height="18" fill="#3498DB"/>
|
||||
<rect x="18" y="30" width="12" height="2" fill="#34495E"/>
|
||||
<rect x="12" y="32" width="24" height="4" rx="1" fill="#2C3E50"/>
|
||||
<circle cx="24" cy="14" r="2" fill="#ECF0F1"/>
|
||||
<rect x="18" y="18" width="12" height="8" rx="1" fill="#ECF0F1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 521 B |
@@ -62,7 +62,7 @@
|
||||
.network-link.active { stroke: #dc2626; stroke-width: 3; }
|
||||
.node-label { font-size: 11px; fill: #a3a3a3; text-anchor: middle; }
|
||||
.node-ip { font-size: 10px; fill: #666; text-anchor: middle; font-family: monospace; }
|
||||
.device-icon { font-size: 24px; }
|
||||
.device-icon { font-size: 24px; font-family: "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif; line-height: 1; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-sp-black text-sp-white">
|
||||
@@ -537,8 +537,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Network Map SVG -->
|
||||
<div x-show="networkHosts.length > 0 && !networkMapLoading" class="flex-1 network-map-container relative">
|
||||
<svg id="networkMapSvg" class="w-full h-full"></svg>
|
||||
<div x-show="networkHosts.length > 0 && !networkMapLoading" class="flex-1 network-map-container relative" style="min-height: 500px; height: calc(100vh - 280px);">
|
||||
<svg id="networkMapSvg" style="width: 100%; height: 100%; display: block;"></svg>
|
||||
</div>
|
||||
|
||||
<!-- Host Details Panel -->
|
||||
@@ -799,6 +799,26 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Show hosts if parsed has hosts (nmap scans) -->
|
||||
<template x-if="selectedScan.parsed && (Array.isArray(selectedScan.parsed) || selectedScan.parsed.hosts)">
|
||||
<div>
|
||||
<h4 class="text-sm text-sp-white-muted mb-2">Discovered Hosts (<span x-text="(Array.isArray(selectedScan.parsed) ? selectedScan.parsed : selectedScan.parsed.hosts || []).length"></span>)</h4>
|
||||
<div class="space-y-2 max-h-48 overflow-y-auto mb-3">
|
||||
<template x-for="host in (Array.isArray(selectedScan.parsed) ? selectedScan.parsed : selectedScan.parsed.hosts || [])" :key="host.ip">
|
||||
<div class="flex items-center gap-3 bg-sp-black p-2 rounded text-sm">
|
||||
<span class="text-sp-red font-mono" x-text="host.ip"></span>
|
||||
<span class="text-sp-white-muted" x-text="host.hostname || ''"></span>
|
||||
<span class="text-green-400 text-xs" x-text="host.ports ? host.ports.filter(p => p.state === 'open').length + ' open ports' : ''"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<button @click="loadScanToNetworkMap(selectedScan)"
|
||||
class="w-full bg-sp-red hover:bg-sp-red-dark px-4 py-2 rounded text-sm transition flex items-center justify-center gap-2">
|
||||
🗺️ Load to Network Map
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="selectedScan.result && selectedScan.result.stdout">
|
||||
<div>
|
||||
<h4 class="text-sm text-sp-white-muted mb-2">Raw Output</h4>
|
||||
@@ -1264,6 +1284,49 @@ Select a phase above to begin, or use the quick actions in the sidebar!`
|
||||
|
||||
viewScanDetails(scan) { this.selectedScan = scan; this.detailsModalOpen = true; },
|
||||
|
||||
loadScanToNetworkMap(scan) {
|
||||
// Get hosts from the scan's parsed data
|
||||
let hosts = [];
|
||||
if (Array.isArray(scan.parsed)) {
|
||||
hosts = scan.parsed;
|
||||
} else if (scan.parsed && scan.parsed.hosts) {
|
||||
hosts = scan.parsed.hosts;
|
||||
}
|
||||
|
||||
if (hosts.length === 0) {
|
||||
alert('No hosts found in this scan');
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge hosts into networkHosts (update existing, add new)
|
||||
hosts.forEach(host => {
|
||||
const existing = this.networkHosts.find(h => h.ip === host.ip);
|
||||
if (existing) {
|
||||
// Update existing host
|
||||
Object.assign(existing, host);
|
||||
} else {
|
||||
// Add new host
|
||||
this.networkHosts.push({
|
||||
ip: host.ip,
|
||||
hostname: host.hostname || '',
|
||||
os_type: host.os_type || host.os || '',
|
||||
os_details: host.os_details || '',
|
||||
ports: host.ports || [],
|
||||
mac: host.mac || '',
|
||||
vendor: host.vendor || '',
|
||||
source: 'scan-history'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal and switch to network map
|
||||
this.detailsModalOpen = false;
|
||||
this.activeTab = 'network-map';
|
||||
|
||||
// Re-render network map
|
||||
this.$nextTick(() => this.renderNetworkMap());
|
||||
},
|
||||
|
||||
askAboutTool(tool) {
|
||||
this.activeTab = 'phase';
|
||||
this.userInput = `Explain how to use ${tool.name} for ${this.getCurrentPhase().name.toLowerCase()}. Include common options and example commands.`;
|
||||
@@ -1569,117 +1632,114 @@ Select a phase above to begin, or use the quick actions in the sidebar!`
|
||||
|
||||
if (this.networkHosts.length === 0) return;
|
||||
|
||||
const container = document.getElementById('networkMapSvg').parentElement;
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
const container = document.getElementById('networkMapSvg');
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const width = rect.width || 800;
|
||||
const height = rect.height || 500;
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Set explicit width/height on SVG
|
||||
svg.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
// Create a gateway node (center)
|
||||
const gatewayIp = this.networkHosts[0]?.ip.split('.').slice(0, 3).join('.') + '.1';
|
||||
const nodes = [
|
||||
{ id: 'gateway', ip: gatewayIp, os_type: 'router', hostname: 'Gateway', isGateway: true }
|
||||
];
|
||||
|
||||
// Add host nodes
|
||||
this.networkHosts.forEach(host => {
|
||||
nodes.push({
|
||||
id: host.ip,
|
||||
ip: host.ip,
|
||||
os_type: host.os_type,
|
||||
hostname: host.hostname,
|
||||
ports: host.ports
|
||||
});
|
||||
// Calculate static positions - gateway in center, hosts in circle
|
||||
const hostCount = this.networkHosts.length;
|
||||
const radius = Math.min(width, height) * 0.35;
|
||||
|
||||
// Draw links first (behind nodes)
|
||||
const linkGroup = svg.append('g');
|
||||
this.networkHosts.forEach((host, i) => {
|
||||
const angle = (2 * Math.PI * i) / hostCount - Math.PI / 2;
|
||||
const hostX = centerX + radius * Math.cos(angle);
|
||||
const hostY = centerY + radius * Math.sin(angle);
|
||||
|
||||
linkGroup.append('line')
|
||||
.attr('x1', centerX)
|
||||
.attr('y1', centerY)
|
||||
.attr('x2', hostX)
|
||||
.attr('y2', hostY)
|
||||
.attr('stroke', '#3a3a3a')
|
||||
.attr('stroke-width', 2);
|
||||
});
|
||||
|
||||
// Create links from each host to gateway
|
||||
const links = this.networkHosts.map(host => ({
|
||||
source: 'gateway',
|
||||
target: host.ip
|
||||
}));
|
||||
// Draw gateway node
|
||||
const gatewayGroup = svg.append('g')
|
||||
.attr('transform', `translate(${centerX},${centerY})`)
|
||||
.style('cursor', 'pointer');
|
||||
|
||||
// Force simulation
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links).id(d => d.id).distance(150))
|
||||
.force('charge', d3.forceManyBody().strength(-300))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
.force('collision', d3.forceCollide().radius(60));
|
||||
gatewayGroup.append('circle')
|
||||
.attr('r', 40)
|
||||
.attr('fill', '#1f1f1f')
|
||||
.attr('stroke', '#dc2626')
|
||||
.attr('stroke-width', 3);
|
||||
|
||||
// Draw links
|
||||
const link = svg.append('g')
|
||||
.selectAll('line')
|
||||
.data(links)
|
||||
.enter().append('line')
|
||||
.attr('class', 'network-link')
|
||||
gatewayGroup.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dy', '0.35em')
|
||||
.attr('font-size', '28px')
|
||||
.text('📡');
|
||||
|
||||
gatewayGroup.append('text')
|
||||
.attr('class', 'node-ip')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dy', 60)
|
||||
.attr('fill', '#666')
|
||||
.attr('font-size', '10px')
|
||||
.attr('font-family', 'monospace')
|
||||
.text(gatewayIp);
|
||||
|
||||
gatewayGroup.append('text')
|
||||
.attr('class', 'node-label')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dy', 75)
|
||||
.attr('fill', '#a3a3a3')
|
||||
.attr('font-size', '11px')
|
||||
.text('Gateway');
|
||||
|
||||
// Draw host nodes
|
||||
this.networkHosts.forEach((host, i) => {
|
||||
const angle = (2 * Math.PI * i) / hostCount - Math.PI / 2;
|
||||
const hostX = centerX + radius * Math.cos(angle);
|
||||
const hostY = centerY + radius * Math.sin(angle);
|
||||
|
||||
const hostGroup = svg.append('g')
|
||||
.attr('transform', `translate(${hostX},${hostY})`)
|
||||
.style('cursor', 'pointer')
|
||||
.on('click', () => this.selectHost(host));
|
||||
|
||||
hostGroup.append('circle')
|
||||
.attr('r', 35)
|
||||
.attr('fill', '#2a2a2a')
|
||||
.attr('stroke', '#3a3a3a')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Draw nodes
|
||||
const node = svg.append('g')
|
||||
.selectAll('g')
|
||||
.data(nodes)
|
||||
.enter().append('g')
|
||||
.attr('class', 'network-node')
|
||||
.attr('data-ip', d => d.ip)
|
||||
.style('cursor', 'pointer')
|
||||
.on('click', (event, d) => {
|
||||
if (!d.isGateway) {
|
||||
const host = this.networkHosts.find(h => h.ip === d.ip);
|
||||
if (host) this.selectHost(host);
|
||||
}
|
||||
});
|
||||
|
||||
// Node circles
|
||||
node.append('circle')
|
||||
.attr('r', d => d.isGateway ? 35 : 30)
|
||||
.attr('fill', d => d.isGateway ? '#1f1f1f' : '#2a2a2a')
|
||||
.attr('stroke', d => d.isGateway ? '#dc2626' : '#3a3a3a')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Device icons
|
||||
node.append('text')
|
||||
.attr('class', 'device-icon')
|
||||
hostGroup.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dy', '0.35em')
|
||||
.text(d => this.getDeviceIcon(d.os_type));
|
||||
.attr('font-size', '24px')
|
||||
.text(this.getDeviceIcon(host.os_type));
|
||||
|
||||
// IP labels
|
||||
node.append('text')
|
||||
.attr('class', 'node-ip')
|
||||
.attr('dy', 50)
|
||||
.text(d => d.ip);
|
||||
hostGroup.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dy', 55)
|
||||
.attr('fill', '#dc2626')
|
||||
.attr('font-size', '11px')
|
||||
.attr('font-family', 'monospace')
|
||||
.text(host.ip);
|
||||
|
||||
// Hostname labels
|
||||
node.append('text')
|
||||
.attr('class', 'node-label')
|
||||
.attr('dy', 65)
|
||||
.text(d => d.hostname || '');
|
||||
|
||||
// Update positions on tick
|
||||
simulation.on('tick', () => {
|
||||
link
|
||||
.attr('x1', d => d.source.x)
|
||||
.attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x)
|
||||
.attr('y2', d => d.target.y);
|
||||
|
||||
node.attr('transform', d => `translate(${d.x},${d.y})`);
|
||||
hostGroup.append('text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dy', 70)
|
||||
.attr('fill', '#a3a3a3')
|
||||
.attr('font-size', '10px')
|
||||
.text(host.hostname ? host.hostname.substring(0, 20) : '');
|
||||
});
|
||||
|
||||
// Drag behavior
|
||||
node.call(d3.drag()
|
||||
.on('start', (event, d) => {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
})
|
||||
.on('drag', (event, d) => {
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
})
|
||||
.on('end', (event, d) => {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
535
services/hackgpt-api/app/config_validator.py
Normal file
@@ -0,0 +1,535 @@
|
||||
"""
|
||||
Configuration Validator Module
|
||||
Validates configurations before save/change with plain-English warnings.
|
||||
Provides backup/restore functionality and auto-fix suggestions.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from datetime import datetime
|
||||
import copy
|
||||
|
||||
|
||||
# Configuration storage (in production, use persistent storage)
|
||||
config_backups: Dict[str, List[Dict[str, Any]]] = {}
|
||||
BACKUP_DIR = os.getenv("CONFIG_BACKUP_DIR", "/workspace/config_backups")
|
||||
|
||||
|
||||
def validate_config(
|
||||
config_data: Dict[str, Any],
|
||||
config_type: str = "general"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate configuration data before applying changes.
|
||||
|
||||
Args:
|
||||
config_data: Configuration dictionary to validate
|
||||
config_type: Type of configuration (scan, system, security, network)
|
||||
|
||||
Returns:
|
||||
Dictionary with validation results
|
||||
{
|
||||
"valid": bool,
|
||||
"warnings": List[str],
|
||||
"errors": List[str],
|
||||
"suggestions": List[Dict],
|
||||
"safe_to_apply": bool
|
||||
}
|
||||
"""
|
||||
errors = []
|
||||
warnings = []
|
||||
suggestions = []
|
||||
|
||||
# Type-specific validation
|
||||
if config_type == "scan":
|
||||
errors, warnings, suggestions = _validate_scan_config(config_data)
|
||||
elif config_type == "network":
|
||||
errors, warnings, suggestions = _validate_network_config(config_data)
|
||||
elif config_type == "security":
|
||||
errors, warnings, suggestions = _validate_security_config(config_data)
|
||||
else:
|
||||
errors, warnings, suggestions = _validate_general_config(config_data)
|
||||
|
||||
# Check for common issues across all config types
|
||||
common_errors, common_warnings = _check_common_issues(config_data)
|
||||
errors.extend(common_errors)
|
||||
warnings.extend(common_warnings)
|
||||
|
||||
return {
|
||||
"valid": len(errors) == 0,
|
||||
"warnings": warnings,
|
||||
"errors": errors,
|
||||
"suggestions": suggestions,
|
||||
"safe_to_apply": len(errors) == 0 and len([w for w in warnings if "critical" in w.lower()]) == 0,
|
||||
"config_type": config_type
|
||||
}
|
||||
|
||||
|
||||
def _validate_scan_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
|
||||
"""Validate scan configuration."""
|
||||
errors = []
|
||||
warnings = []
|
||||
suggestions = []
|
||||
|
||||
# Check timeout
|
||||
timeout = config_data.get("timeout", 300)
|
||||
if not isinstance(timeout, (int, float)):
|
||||
errors.append("Timeout must be a number (seconds)")
|
||||
elif timeout < 1:
|
||||
errors.append("Timeout must be at least 1 second")
|
||||
elif timeout < 10:
|
||||
warnings.append("Very short timeout (< 10s) may cause scans to fail prematurely")
|
||||
elif timeout > 3600:
|
||||
warnings.append("Very long timeout (> 1 hour) may cause scans to hang indefinitely")
|
||||
|
||||
# Check target
|
||||
target = config_data.get("target", "")
|
||||
if not target or not isinstance(target, str):
|
||||
errors.append("Target must be specified (IP address, hostname, or network range)")
|
||||
elif not _is_valid_target(target):
|
||||
warnings.append(f"Target '{target}' may not be valid - ensure it's a valid IP, hostname, or CIDR")
|
||||
|
||||
# Check scan intensity
|
||||
intensity = config_data.get("intensity", 3)
|
||||
if isinstance(intensity, (int, float)):
|
||||
if intensity < 1 or intensity > 5:
|
||||
warnings.append("Scan intensity should be between 1 (stealth) and 5 (aggressive)")
|
||||
if intensity >= 4:
|
||||
warnings.append("High intensity scans may trigger IDS/IPS systems")
|
||||
suggestions.append({
|
||||
"field": "intensity",
|
||||
"suggestion": 3,
|
||||
"reason": "Balanced intensity for stealth and speed"
|
||||
})
|
||||
|
||||
# Check port range
|
||||
ports = config_data.get("ports", "")
|
||||
if ports:
|
||||
if not _is_valid_port_spec(str(ports)):
|
||||
errors.append(f"Invalid port specification: {ports}")
|
||||
|
||||
return errors, warnings, suggestions
|
||||
|
||||
|
||||
def _validate_network_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
|
||||
"""Validate network configuration."""
|
||||
errors = []
|
||||
warnings = []
|
||||
suggestions = []
|
||||
|
||||
# Check port
|
||||
port = config_data.get("port")
|
||||
if port is not None:
|
||||
if not isinstance(port, int):
|
||||
errors.append("Port must be an integer")
|
||||
elif port < 1 or port > 65535:
|
||||
errors.append("Port must be between 1 and 65535")
|
||||
elif port < 1024:
|
||||
warnings.append("Ports below 1024 require elevated privileges")
|
||||
|
||||
# Check host/bind address
|
||||
host = config_data.get("host", "")
|
||||
if host and not _is_valid_ip_or_hostname(host):
|
||||
warnings.append(f"Host '{host}' may not be a valid IP address or hostname")
|
||||
|
||||
# Check max connections
|
||||
max_conn = config_data.get("max_connections")
|
||||
if max_conn is not None:
|
||||
if not isinstance(max_conn, int) or max_conn < 1:
|
||||
errors.append("max_connections must be a positive integer")
|
||||
elif max_conn > 1000:
|
||||
warnings.append("Very high max_connections (> 1000) may exhaust system resources")
|
||||
|
||||
return errors, warnings, suggestions
|
||||
|
||||
|
||||
def _validate_security_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
|
||||
"""Validate security configuration."""
|
||||
errors = []
|
||||
warnings = []
|
||||
suggestions = []
|
||||
|
||||
# Check for exposed secrets
|
||||
for key, value in config_data.items():
|
||||
if any(secret_word in key.lower() for secret_word in ['password', 'secret', 'token', 'key', 'credential']):
|
||||
if isinstance(value, str):
|
||||
if len(value) < 8:
|
||||
warnings.append(f"SECURITY: {key} appears weak (< 8 characters)")
|
||||
if value in ['password', '123456', 'admin', 'default']:
|
||||
errors.append(f"SECURITY: {key} is using a default/weak value")
|
||||
|
||||
# Check SSL/TLS settings
|
||||
ssl_enabled = config_data.get("ssl_enabled", False)
|
||||
if not ssl_enabled:
|
||||
warnings.append("SECURITY: SSL/TLS is disabled - data will be transmitted unencrypted")
|
||||
|
||||
# Check authentication
|
||||
auth_enabled = config_data.get("authentication_enabled", True)
|
||||
if not auth_enabled:
|
||||
warnings.append("SECURITY: Authentication is disabled - system will be exposed")
|
||||
|
||||
return errors, warnings, suggestions
|
||||
|
||||
|
||||
def _validate_general_config(config_data: Dict[str, Any]) -> Tuple[List[str], List[str], List[Dict]]:
|
||||
"""Validate general configuration."""
|
||||
errors = []
|
||||
warnings = []
|
||||
suggestions = []
|
||||
|
||||
# Check for valid JSON structure
|
||||
if not isinstance(config_data, dict):
|
||||
errors.append("Configuration must be a JSON object")
|
||||
return errors, warnings, suggestions
|
||||
|
||||
# Check for empty config
|
||||
if not config_data:
|
||||
warnings.append("Configuration is empty")
|
||||
|
||||
return errors, warnings, suggestions
|
||||
|
||||
|
||||
def _check_common_issues(config_data: Dict[str, Any]) -> Tuple[List[str], List[str]]:
|
||||
"""Check for common configuration issues."""
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Validate that config_data is a dict and not too large
|
||||
if not isinstance(config_data, dict):
|
||||
errors.append("Configuration must be a dictionary")
|
||||
return errors, warnings
|
||||
|
||||
if len(config_data) > 1000:
|
||||
warnings.append("Configuration has unusually large number of keys (>1000)")
|
||||
|
||||
# Check for null/undefined values
|
||||
for key, value in config_data.items():
|
||||
# Validate key is a string
|
||||
if not isinstance(key, str):
|
||||
warnings.append(f"Configuration key {key} is not a string")
|
||||
continue
|
||||
|
||||
if value is None:
|
||||
warnings.append(f"Value for '{key}' is null - will use default")
|
||||
|
||||
# Check for suspicious paths
|
||||
for key, value in config_data.items():
|
||||
if isinstance(value, str):
|
||||
if value.startswith('/root/') or value.startswith('C:\\Windows\\'):
|
||||
warnings.append(f"SECURITY: '{key}' points to a sensitive system path")
|
||||
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def backup_config(config_name: str, config_data: Dict[str, Any], description: str = "") -> Dict[str, Any]:
|
||||
"""
|
||||
Create a backup of current configuration.
|
||||
|
||||
Args:
|
||||
config_name: Name/ID of the configuration
|
||||
config_data: Configuration data to backup
|
||||
description: Optional description of the backup
|
||||
|
||||
Returns:
|
||||
Dictionary with backup information
|
||||
"""
|
||||
timestamp = datetime.utcnow().isoformat()
|
||||
backup_id = f"{config_name}_{timestamp}"
|
||||
|
||||
backup = {
|
||||
"backup_id": backup_id,
|
||||
"config_name": config_name,
|
||||
"timestamp": timestamp,
|
||||
"description": description or "Automatic backup",
|
||||
"config_data": copy.deepcopy(config_data),
|
||||
"size_bytes": len(json.dumps(config_data))
|
||||
}
|
||||
|
||||
# Store in memory
|
||||
if config_name not in config_backups:
|
||||
config_backups[config_name] = []
|
||||
config_backups[config_name].append(backup)
|
||||
|
||||
# Keep only last 10 backups per config
|
||||
if len(config_backups[config_name]) > 10:
|
||||
config_backups[config_name] = config_backups[config_name][-10:]
|
||||
|
||||
# Also save to disk if backup directory exists
|
||||
try:
|
||||
os.makedirs(BACKUP_DIR, exist_ok=True)
|
||||
backup_file = os.path.join(BACKUP_DIR, f"{backup_id}.json")
|
||||
with open(backup_file, 'w') as f:
|
||||
json.dump(backup, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not save backup to disk: {e}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"backup_id": backup_id,
|
||||
"timestamp": timestamp,
|
||||
"message": f"Configuration backed up successfully"
|
||||
}
|
||||
|
||||
|
||||
def restore_config(backup_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Restore configuration from a backup.
|
||||
|
||||
Args:
|
||||
backup_id: ID of the backup to restore
|
||||
|
||||
Returns:
|
||||
Dictionary with restored configuration and metadata
|
||||
"""
|
||||
# Search in memory backups
|
||||
for config_name, backups in config_backups.items():
|
||||
for backup in backups:
|
||||
if backup["backup_id"] == backup_id:
|
||||
return {
|
||||
"success": True,
|
||||
"backup_id": backup_id,
|
||||
"config_name": config_name,
|
||||
"config_data": copy.deepcopy(backup["config_data"]),
|
||||
"timestamp": backup["timestamp"],
|
||||
"description": backup["description"],
|
||||
"message": "Configuration restored successfully"
|
||||
}
|
||||
|
||||
# Try loading from disk
|
||||
try:
|
||||
backup_file = os.path.join(BACKUP_DIR, f"{backup_id}.json")
|
||||
if os.path.exists(backup_file):
|
||||
with open(backup_file, 'r') as f:
|
||||
backup = json.load(f)
|
||||
return {
|
||||
"success": True,
|
||||
"backup_id": backup_id,
|
||||
"config_name": backup["config_name"],
|
||||
"config_data": backup["config_data"],
|
||||
"timestamp": backup["timestamp"],
|
||||
"description": backup.get("description", ""),
|
||||
"message": "Configuration restored from disk backup"
|
||||
}
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"backup_id": backup_id,
|
||||
"error": "Backup not found",
|
||||
"message": f"No backup found with ID: {backup_id}"
|
||||
}
|
||||
|
||||
|
||||
def suggest_autofix(validation_result: Dict[str, Any], config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Suggest automatic fixes for configuration issues.
|
||||
|
||||
Args:
|
||||
validation_result: Result from validate_config()
|
||||
config_data: Original configuration data
|
||||
|
||||
Returns:
|
||||
Dictionary with auto-fix suggestions
|
||||
"""
|
||||
if validation_result.get("valid") and not validation_result.get("warnings"):
|
||||
return {
|
||||
"has_fixes": False,
|
||||
"message": "Configuration is valid, no fixes needed"
|
||||
}
|
||||
|
||||
fixed_config = copy.deepcopy(config_data)
|
||||
fixes_applied = []
|
||||
|
||||
# Apply suggestions from validation
|
||||
for suggestion in validation_result.get("suggestions", []):
|
||||
field = suggestion.get("field")
|
||||
suggested_value = suggestion.get("suggestion")
|
||||
reason = suggestion.get("reason")
|
||||
|
||||
if field in fixed_config:
|
||||
old_value = fixed_config[field]
|
||||
fixed_config[field] = suggested_value
|
||||
fixes_applied.append({
|
||||
"field": field,
|
||||
"old_value": old_value,
|
||||
"new_value": suggested_value,
|
||||
"reason": reason
|
||||
})
|
||||
|
||||
# Apply common fixes based on errors
|
||||
for error in validation_result.get("errors", []):
|
||||
if "timeout must be" in error.lower():
|
||||
if "timeout" in fixed_config:
|
||||
fixed_config["timeout"] = 300 # Default safe timeout
|
||||
fixes_applied.append({
|
||||
"field": "timeout",
|
||||
"old_value": config_data.get("timeout"),
|
||||
"new_value": 300,
|
||||
"reason": "Reset to safe default value"
|
||||
})
|
||||
|
||||
if "port must be" in error.lower():
|
||||
if "port" in fixed_config:
|
||||
fixed_config["port"] = 8080 # Default safe port
|
||||
fixes_applied.append({
|
||||
"field": "port",
|
||||
"old_value": config_data.get("port"),
|
||||
"new_value": 8080,
|
||||
"reason": "Reset to safe default port"
|
||||
})
|
||||
|
||||
return {
|
||||
"has_fixes": len(fixes_applied) > 0,
|
||||
"fixes_applied": fixes_applied,
|
||||
"fixed_config": fixed_config,
|
||||
"message": f"Applied {len(fixes_applied)} automatic fixes"
|
||||
}
|
||||
|
||||
|
||||
def list_backups(config_name: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
List available configuration backups.
|
||||
|
||||
Args:
|
||||
config_name: Optional config name to filter by
|
||||
|
||||
Returns:
|
||||
Dictionary with list of backups
|
||||
"""
|
||||
all_backups = []
|
||||
|
||||
# Get from memory
|
||||
if config_name:
|
||||
backups = config_backups.get(config_name, [])
|
||||
for backup in backups:
|
||||
all_backups.append({
|
||||
"backup_id": backup["backup_id"],
|
||||
"config_name": backup["config_name"],
|
||||
"timestamp": backup["timestamp"],
|
||||
"description": backup["description"],
|
||||
"size_bytes": backup["size_bytes"]
|
||||
})
|
||||
else:
|
||||
for cfg_name, backups in config_backups.items():
|
||||
for backup in backups:
|
||||
all_backups.append({
|
||||
"backup_id": backup["backup_id"],
|
||||
"config_name": backup["config_name"],
|
||||
"timestamp": backup["timestamp"],
|
||||
"description": backup["description"],
|
||||
"size_bytes": backup["size_bytes"]
|
||||
})
|
||||
|
||||
# Also check disk backups
|
||||
try:
|
||||
if os.path.exists(BACKUP_DIR):
|
||||
for filename in os.listdir(BACKUP_DIR):
|
||||
if filename.endswith('.json'):
|
||||
backup_id = filename[:-5] # Remove .json
|
||||
# Check if already in list (avoid duplicates)
|
||||
if not any(b["backup_id"] == backup_id for b in all_backups):
|
||||
try:
|
||||
filepath = os.path.join(BACKUP_DIR, filename)
|
||||
with open(filepath, 'r') as f:
|
||||
backup = json.load(f)
|
||||
if not config_name or backup["config_name"] == config_name:
|
||||
all_backups.append({
|
||||
"backup_id": backup["backup_id"],
|
||||
"config_name": backup["config_name"],
|
||||
"timestamp": backup["timestamp"],
|
||||
"description": backup.get("description", ""),
|
||||
"size_bytes": os.path.getsize(filepath)
|
||||
})
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not read disk backups: {e}")
|
||||
|
||||
# Sort by timestamp (newest first)
|
||||
all_backups.sort(key=lambda x: x["timestamp"], reverse=True)
|
||||
|
||||
return {
|
||||
"backups": all_backups,
|
||||
"count": len(all_backups),
|
||||
"config_name": config_name
|
||||
}
|
||||
|
||||
|
||||
# Validation helper functions
|
||||
|
||||
def _is_valid_target(target: str) -> bool:
|
||||
"""Check if target is a valid IP, hostname, or CIDR."""
|
||||
import re
|
||||
|
||||
# IP address
|
||||
ip_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
|
||||
if re.match(ip_pattern, target):
|
||||
parts = target.split('.')
|
||||
try:
|
||||
return all(0 <= int(part) <= 255 for part in parts)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# CIDR notation
|
||||
if '/' in target:
|
||||
cidr_pattern = r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$'
|
||||
if re.match(cidr_pattern, target):
|
||||
ip_part = target.split('/')[0]
|
||||
return _is_valid_target(ip_part)
|
||||
|
||||
# IP range
|
||||
if '-' in target:
|
||||
range_pattern = r'^(\d{1,3}\.){3}\d{1,3}-\d{1,3}$'
|
||||
if re.match(range_pattern, target):
|
||||
return True
|
||||
|
||||
# Hostname/domain
|
||||
hostname_pattern = r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$'
|
||||
if re.match(hostname_pattern, target):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _is_valid_port_spec(ports: str) -> bool:
|
||||
"""Check if port specification is valid."""
|
||||
import re
|
||||
|
||||
# Single port
|
||||
if ports.isdigit():
|
||||
port_num = int(ports)
|
||||
return 1 <= port_num <= 65535
|
||||
|
||||
# Port range
|
||||
if '-' in ports:
|
||||
range_pattern = r'^\d+-\d+$'
|
||||
if re.match(range_pattern, ports):
|
||||
start, end = map(int, ports.split('-'))
|
||||
return 1 <= start <= end <= 65535
|
||||
|
||||
# Comma-separated ports
|
||||
if ',' in ports:
|
||||
port_list = ports.split(',')
|
||||
return all(_is_valid_port_spec(p.strip()) for p in port_list)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _is_valid_ip_or_hostname(host: str) -> bool:
|
||||
"""Check if host is a valid IP address or hostname."""
|
||||
import re
|
||||
|
||||
# IP address
|
||||
ip_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
|
||||
if re.match(ip_pattern, host):
|
||||
parts = host.split('.')
|
||||
try:
|
||||
return all(0 <= int(part) <= 255 for part in parts)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# Hostname
|
||||
hostname_pattern = r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$'
|
||||
return bool(re.match(hostname_pattern, host))
|
||||
547
services/hackgpt-api/app/explain.py
Normal file
@@ -0,0 +1,547 @@
|
||||
"""
|
||||
Explain Module
|
||||
Provides "Explain this" functionality for configs, logs, errors, and onboarding.
|
||||
Generates plain-English explanations and suggestions for fixes.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
import re
|
||||
import os
|
||||
|
||||
|
||||
def explain_config(config_key: str, config_value: Any, context: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Explain a configuration setting in plain English.
|
||||
|
||||
Args:
|
||||
config_key: Configuration key/name
|
||||
config_value: Current value of the configuration
|
||||
context: Additional context about the configuration
|
||||
|
||||
Returns:
|
||||
Dictionary with explanation and recommendations
|
||||
"""
|
||||
# Common configuration patterns and their explanations
|
||||
config_patterns = {
|
||||
r'.*timeout.*': {
|
||||
'description': 'Controls how long the system waits before giving up on an operation',
|
||||
'example': 'A timeout of 30 seconds means operations will be cancelled after 30s',
|
||||
'recommendations': [
|
||||
'Increase timeout for slow networks or large scans',
|
||||
'Decrease timeout for faster detection of unavailable services',
|
||||
'Typical values: 10-300 seconds'
|
||||
]
|
||||
},
|
||||
r'.*port.*': {
|
||||
'description': 'Specifies which network port to use for communication',
|
||||
'example': 'Port 8080 is commonly used for web applications',
|
||||
'recommendations': [
|
||||
'Use standard ports (80/443) for production',
|
||||
'Use high ports (8000+) for development',
|
||||
'Ensure port is not blocked by firewall'
|
||||
]
|
||||
},
|
||||
r'.*api[_-]?key.*': {
|
||||
'description': 'Authentication key for accessing external services',
|
||||
'example': 'API keys should be kept secret and not shared publicly',
|
||||
'recommendations': [
|
||||
'Store API keys in environment variables',
|
||||
'Never commit API keys to version control',
|
||||
'Rotate keys regularly for security'
|
||||
]
|
||||
},
|
||||
r'.*thread.*|.*worker.*': {
|
||||
'description': 'Controls parallel processing and concurrency',
|
||||
'example': '4 workers means 4 operations can run simultaneously',
|
||||
'recommendations': [
|
||||
'More workers = faster but more resource usage',
|
||||
'Typical range: number of CPU cores or 2x CPU cores',
|
||||
'Too many workers can overwhelm the system'
|
||||
]
|
||||
},
|
||||
r'.*rate[_-]?limit.*': {
|
||||
'description': 'Limits the frequency of operations to prevent overload',
|
||||
'example': 'Rate limit of 100/minute means max 100 requests per minute',
|
||||
'recommendations': [
|
||||
'Set based on target system capabilities',
|
||||
'Lower for sensitive or production targets',
|
||||
'Higher for testing environments'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Find matching pattern
|
||||
explanation = {
|
||||
'description': 'Configuration setting',
|
||||
'example': '',
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
for pattern, details in config_patterns.items():
|
||||
if re.search(pattern, config_key, re.IGNORECASE):
|
||||
explanation = details
|
||||
break
|
||||
|
||||
# Value-specific analysis
|
||||
value_analysis = _analyze_config_value(config_key, config_value)
|
||||
|
||||
return {
|
||||
'config_key': config_key,
|
||||
'current_value': str(config_value),
|
||||
'description': explanation['description'],
|
||||
'example': explanation['example'],
|
||||
'recommendations': explanation['recommendations'],
|
||||
'value_analysis': value_analysis,
|
||||
'safe_to_change': _is_safe_to_change(config_key),
|
||||
'requires_restart': _requires_restart(config_key)
|
||||
}
|
||||
|
||||
|
||||
def explain_error(error_message: str, error_type: Optional[str] = None, context: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Explain an error message in plain English with suggested fixes.
|
||||
|
||||
Args:
|
||||
error_message: The error message text
|
||||
error_type: Type/category of error (if known)
|
||||
context: Additional context about where/when the error occurred
|
||||
|
||||
Returns:
|
||||
Dictionary with explanation and fix suggestions
|
||||
"""
|
||||
# Common error patterns
|
||||
error_patterns = [
|
||||
{
|
||||
'pattern': r'connection\s+(refused|timed?\s?out|failed|reset)',
|
||||
'plain_english': 'Unable to connect to the target',
|
||||
'likely_causes': [
|
||||
'Target is offline or unreachable',
|
||||
'Firewall blocking the connection',
|
||||
'Wrong IP address or port',
|
||||
'Network connectivity issues'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Verify target IP address is correct',
|
||||
'Check if target is online (ping test)',
|
||||
'Ensure no firewall is blocking the connection',
|
||||
'Try a different port or protocol'
|
||||
]
|
||||
},
|
||||
{
|
||||
'pattern': r'permission\s+denied|access\s+denied|forbidden',
|
||||
'plain_english': 'You don\'t have permission to perform this action',
|
||||
'likely_causes': [
|
||||
'Insufficient user privileges',
|
||||
'Authentication failed',
|
||||
'Resource is protected',
|
||||
'Rate limiting in effect'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Run with appropriate privileges (sudo if needed)',
|
||||
'Check authentication credentials',
|
||||
'Verify you have permission to access this resource',
|
||||
'Wait before retrying (if rate limited)'
|
||||
]
|
||||
},
|
||||
{
|
||||
'pattern': r'not\s+found|does\s+not\s+exist|no\s+such',
|
||||
'plain_english': 'The requested resource could not be found',
|
||||
'likely_causes': [
|
||||
'Resource has been moved or deleted',
|
||||
'Incorrect path or name',
|
||||
'Typo in the request',
|
||||
'Resource not yet created'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Check spelling and capitalization',
|
||||
'Verify the resource exists',
|
||||
'Check if path or URL is correct',
|
||||
'Create the resource if needed'
|
||||
]
|
||||
},
|
||||
{
|
||||
'pattern': r'invalid\s+(argument|parameter|input|syntax)',
|
||||
'plain_english': 'The input provided is not valid or in the wrong format',
|
||||
'likely_causes': [
|
||||
'Wrong data type or format',
|
||||
'Missing required parameter',
|
||||
'Value out of valid range',
|
||||
'Syntax error in command'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Check documentation for correct format',
|
||||
'Verify all required parameters are provided',
|
||||
'Ensure values are within valid ranges',
|
||||
'Check for typos in the command'
|
||||
]
|
||||
},
|
||||
{
|
||||
'pattern': r'timeout|timed\s+out',
|
||||
'plain_english': 'The operation took too long and was cancelled',
|
||||
'likely_causes': [
|
||||
'Network is slow or congested',
|
||||
'Target is responding slowly',
|
||||
'Timeout setting is too low',
|
||||
'Large operation needs more time'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Increase timeout value in settings',
|
||||
'Check network connectivity',
|
||||
'Try again during off-peak hours',
|
||||
'Break operation into smaller parts'
|
||||
]
|
||||
},
|
||||
{
|
||||
'pattern': r'out\s+of\s+memory|memory\s+error',
|
||||
'plain_english': 'The system ran out of available memory',
|
||||
'likely_causes': [
|
||||
'Too many concurrent operations',
|
||||
'Processing too much data at once',
|
||||
'Memory leak in the application',
|
||||
'Insufficient system resources'
|
||||
],
|
||||
'suggested_fixes': [
|
||||
'Reduce number of concurrent operations',
|
||||
'Process data in smaller batches',
|
||||
'Restart the application',
|
||||
'Add more RAM to the system'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# Find matching pattern
|
||||
match_result = {
|
||||
'plain_english': 'An error occurred',
|
||||
'likely_causes': ['Unknown error condition'],
|
||||
'suggested_fixes': ['Check logs for more details', 'Try the operation again']
|
||||
}
|
||||
|
||||
error_lower = error_message.lower()
|
||||
for pattern_info in error_patterns:
|
||||
if re.search(pattern_info['pattern'], error_lower):
|
||||
match_result = {
|
||||
'plain_english': pattern_info['plain_english'],
|
||||
'likely_causes': pattern_info['likely_causes'],
|
||||
'suggested_fixes': pattern_info['suggested_fixes']
|
||||
}
|
||||
break
|
||||
|
||||
return {
|
||||
'original_error': error_message,
|
||||
'error_type': error_type or 'unknown',
|
||||
'plain_english': match_result['plain_english'],
|
||||
'likely_causes': match_result['likely_causes'],
|
||||
'suggested_fixes': match_result['suggested_fixes'],
|
||||
'severity': _assess_error_severity(error_message),
|
||||
'context': context or {}
|
||||
}
|
||||
|
||||
|
||||
def explain_log_entry(log_entry: str, log_level: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Explain a log entry in plain English.
|
||||
|
||||
Args:
|
||||
log_entry: The log message text
|
||||
log_level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
|
||||
Returns:
|
||||
Dictionary with explanation of the log entry
|
||||
"""
|
||||
# Detect log level if not provided
|
||||
if not log_level:
|
||||
log_level = _detect_log_level(log_entry)
|
||||
|
||||
# Extract key information from log
|
||||
extracted_info = _extract_log_info(log_entry)
|
||||
|
||||
# Determine if action is needed
|
||||
action_needed = log_level in ['ERROR', 'CRITICAL', 'WARNING']
|
||||
|
||||
explanation = {
|
||||
'log_entry': log_entry,
|
||||
'log_level': log_level,
|
||||
'timestamp': extracted_info.get('timestamp'),
|
||||
'component': extracted_info.get('component'),
|
||||
'message': extracted_info.get('message', log_entry),
|
||||
'action_needed': action_needed,
|
||||
'explanation': _generate_log_explanation(log_entry, log_level),
|
||||
'next_steps': _suggest_log_next_steps(log_entry, log_level) if action_needed else []
|
||||
}
|
||||
|
||||
return explanation
|
||||
|
||||
|
||||
def get_wizard_step_help(wizard_type: str, step_number: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Get help text for a specific wizard step.
|
||||
|
||||
Args:
|
||||
wizard_type: Type of wizard (create_operation, onboard_agent, run_scan, first_time_setup)
|
||||
step_number: Current step number (1-indexed)
|
||||
|
||||
Returns:
|
||||
Dictionary with help information for the step
|
||||
"""
|
||||
wizard_help = {
|
||||
'create_operation': {
|
||||
1: {
|
||||
'title': 'Operation Name and Type',
|
||||
'description': 'Give your operation a memorable name and select the type of security assessment',
|
||||
'tips': [
|
||||
'Use descriptive names like "Q4 External Assessment" or "Web App Pentest"',
|
||||
'Choose the operation type that matches your goals',
|
||||
'You can change these later in settings'
|
||||
],
|
||||
'example': 'Example: "Internal Network Audit - Production"'
|
||||
},
|
||||
2: {
|
||||
'title': 'Define Target Scope',
|
||||
'description': 'Specify which systems, networks, or applications to include in the assessment',
|
||||
'tips': [
|
||||
'Use CIDR notation for network ranges (e.g., 192.168.1.0/24)',
|
||||
'Add individual hosts or domains as needed',
|
||||
'Clearly define what is in-scope and out-of-scope'
|
||||
],
|
||||
'example': 'Example: 192.168.1.0/24, app.example.com'
|
||||
},
|
||||
3: {
|
||||
'title': 'Configure Assessment Tools',
|
||||
'description': 'Select which security tools to use and configure their settings',
|
||||
'tips': [
|
||||
'Start with reconnaissance tools (nmap, whatweb)',
|
||||
'Add vulnerability scanners based on target type',
|
||||
'Adjust scan intensity based on target sensitivity'
|
||||
],
|
||||
'example': 'Example: nmap (aggressive), nikto (web servers only)'
|
||||
}
|
||||
},
|
||||
'run_scan': {
|
||||
1: {
|
||||
'title': 'Select Scan Tool',
|
||||
'description': 'Choose the security tool appropriate for your target',
|
||||
'tips': [
|
||||
'nmap: Network scanning and service detection',
|
||||
'nikto: Web server vulnerability scanning',
|
||||
'gobuster: Directory and file discovery',
|
||||
'sqlmap: SQL injection testing'
|
||||
],
|
||||
'example': 'For a web server, use nikto or gobuster'
|
||||
},
|
||||
2: {
|
||||
'title': 'Specify Target',
|
||||
'description': 'Enter the IP address, hostname, or network range to scan',
|
||||
'tips': [
|
||||
'Single host: 192.168.1.100 or example.com',
|
||||
'Network range: 192.168.1.0/24',
|
||||
'Multiple hosts: 192.168.1.1-50'
|
||||
],
|
||||
'example': 'Example: 192.168.1.0/24 for entire subnet'
|
||||
},
|
||||
3: {
|
||||
'title': 'Scan Options',
|
||||
'description': 'Configure scan parameters and intensity',
|
||||
'tips': [
|
||||
'Quick scan: Fast but less thorough',
|
||||
'Full scan: Comprehensive but slower',
|
||||
'Stealth: Slower but harder to detect'
|
||||
],
|
||||
'example': 'Use quick scan for initial reconnaissance'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
steps = wizard_help.get(wizard_type, {})
|
||||
step_help = steps.get(step_number, {
|
||||
'title': f'Step {step_number}',
|
||||
'description': 'Complete this step to continue',
|
||||
'tips': ['Fill in the required information'],
|
||||
'example': ''
|
||||
})
|
||||
|
||||
return {
|
||||
'wizard_type': wizard_type,
|
||||
'step_number': step_number,
|
||||
'total_steps': len(steps),
|
||||
**step_help
|
||||
}
|
||||
|
||||
|
||||
def suggest_fix(issue_description: str, context: Optional[Dict] = None) -> List[str]:
|
||||
"""
|
||||
Suggest fixes for a described issue.
|
||||
|
||||
Args:
|
||||
issue_description: Description of the problem
|
||||
context: Additional context (error codes, logs, etc.)
|
||||
|
||||
Returns:
|
||||
List of suggested fix actions
|
||||
"""
|
||||
issue_lower = issue_description.lower()
|
||||
fixes = []
|
||||
|
||||
# Connectivity issues
|
||||
if any(word in issue_lower for word in ['connect', 'network', 'reach', 'timeout']):
|
||||
fixes.extend([
|
||||
'Verify target is online with ping test',
|
||||
'Check firewall rules and network connectivity',
|
||||
'Ensure correct IP address and port number',
|
||||
'Try increasing timeout value in settings'
|
||||
])
|
||||
|
||||
# Permission issues
|
||||
if any(word in issue_lower for word in ['permission', 'access', 'denied', 'forbidden']):
|
||||
fixes.extend([
|
||||
'Run with elevated privileges (sudo)',
|
||||
'Check file/directory permissions',
|
||||
'Verify authentication credentials',
|
||||
'Ensure user has required roles/permissions'
|
||||
])
|
||||
|
||||
# Configuration issues
|
||||
if any(word in issue_lower for word in ['config', 'setting', 'option']):
|
||||
fixes.extend([
|
||||
'Review configuration file for errors',
|
||||
'Restore default configuration',
|
||||
'Check configuration documentation',
|
||||
'Validate configuration format (JSON/YAML)'
|
||||
])
|
||||
|
||||
# Tool/command issues
|
||||
if any(word in issue_lower for word in ['command', 'tool', 'not found', 'install']):
|
||||
fixes.extend([
|
||||
'Install the required tool or package',
|
||||
'Check if tool is in system PATH',
|
||||
'Verify tool name spelling',
|
||||
'Update tool to latest version'
|
||||
])
|
||||
|
||||
# Default suggestions if no specific fix found
|
||||
if not fixes:
|
||||
fixes = [
|
||||
'Check system logs for more details',
|
||||
'Restart the affected service',
|
||||
'Review recent configuration changes',
|
||||
'Consult documentation or support'
|
||||
]
|
||||
|
||||
return fixes[:5] # Return top 5 suggestions
|
||||
|
||||
|
||||
# Helper functions
|
||||
|
||||
def _analyze_config_value(key: str, value: Any) -> str:
|
||||
"""Analyze a configuration value and provide feedback."""
|
||||
if isinstance(value, int):
|
||||
if 'timeout' in key.lower():
|
||||
if value < 10:
|
||||
return 'Very low - may cause premature failures'
|
||||
elif value > 300:
|
||||
return 'Very high - operations may take long to fail'
|
||||
else:
|
||||
return 'Reasonable value'
|
||||
elif 'port' in key.lower():
|
||||
if value < 1024:
|
||||
return 'System port - requires elevated privileges'
|
||||
else:
|
||||
return 'User port - no special privileges needed'
|
||||
|
||||
return 'Current value seems valid'
|
||||
|
||||
|
||||
def _is_safe_to_change(config_key: str) -> bool:
|
||||
"""Determine if a config is safe to change without risk."""
|
||||
unsafe_keys = ['database', 'credential', 'key', 'secret', 'password']
|
||||
return not any(unsafe in config_key.lower() for unsafe in unsafe_keys)
|
||||
|
||||
|
||||
def _requires_restart(config_key: str) -> bool:
|
||||
"""Determine if changing this config requires a restart."""
|
||||
restart_keys = ['port', 'host', 'database', 'worker', 'thread']
|
||||
return any(key in config_key.lower() for key in restart_keys)
|
||||
|
||||
|
||||
def _assess_error_severity(error_message: str) -> str:
|
||||
"""Assess the severity of an error."""
|
||||
error_lower = error_message.lower()
|
||||
|
||||
if any(word in error_lower for word in ['critical', 'fatal', 'crash', 'panic']):
|
||||
return 'critical'
|
||||
elif any(word in error_lower for word in ['error', 'fail', 'exception']):
|
||||
return 'high'
|
||||
elif any(word in error_lower for word in ['warning', 'warn']):
|
||||
return 'medium'
|
||||
else:
|
||||
return 'low'
|
||||
|
||||
|
||||
def _detect_log_level(log_entry: str) -> str:
|
||||
"""Detect log level from log entry."""
|
||||
levels = ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']
|
||||
for level in levels:
|
||||
if level in log_entry.upper():
|
||||
return level
|
||||
return 'INFO'
|
||||
|
||||
|
||||
def _extract_log_info(log_entry: str) -> Dict[str, str]:
|
||||
"""Extract structured information from a log entry."""
|
||||
info = {}
|
||||
|
||||
# Try to extract timestamp
|
||||
timestamp_pattern = r'\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}'
|
||||
timestamp_match = re.search(timestamp_pattern, log_entry)
|
||||
if timestamp_match:
|
||||
info['timestamp'] = timestamp_match.group()
|
||||
|
||||
# Try to extract component/module name
|
||||
component_pattern = r'\[(\w+)\]'
|
||||
component_match = re.search(component_pattern, log_entry)
|
||||
if component_match:
|
||||
info['component'] = component_match.group(1)
|
||||
|
||||
# Extract the main message
|
||||
parts = log_entry.split(':', 1)
|
||||
if len(parts) > 1:
|
||||
info['message'] = parts[1].strip()
|
||||
else:
|
||||
info['message'] = log_entry
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def _generate_log_explanation(log_entry: str, log_level: str) -> str:
|
||||
"""Generate a plain English explanation of a log entry."""
|
||||
if log_level == 'ERROR':
|
||||
return 'An error occurred that may require attention. Check the details to understand what went wrong.'
|
||||
elif log_level == 'WARNING':
|
||||
return 'A potential issue was detected. It may not be critical but should be reviewed.'
|
||||
elif log_level == 'INFO':
|
||||
return 'Normal operational message providing status information.'
|
||||
elif log_level == 'DEBUG':
|
||||
return 'Detailed diagnostic information useful for troubleshooting.'
|
||||
else:
|
||||
return 'Log entry documenting system activity.'
|
||||
|
||||
|
||||
def _suggest_log_next_steps(log_entry: str, log_level: str) -> List[str]:
|
||||
"""Suggest next steps based on log entry."""
|
||||
steps = []
|
||||
|
||||
if log_level in ['ERROR', 'CRITICAL']:
|
||||
steps.append('Review the error details and check related logs')
|
||||
steps.append('Check if the issue is repeating or isolated')
|
||||
steps.append('Consider rolling back recent changes if applicable')
|
||||
|
||||
if log_level == 'WARNING':
|
||||
steps.append('Monitor for repeated warnings')
|
||||
steps.append('Check if this indicates a trend or pattern')
|
||||
|
||||
if 'connection' in log_entry.lower():
|
||||
steps.append('Verify network connectivity to the target')
|
||||
|
||||
if 'timeout' in log_entry.lower():
|
||||
steps.append('Consider increasing timeout values')
|
||||
|
||||
return steps
|
||||
@@ -1,21 +0,0 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
import os
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Allows all origins
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"], # Allows all methods
|
||||
allow_headers=["*"], # Allows all headers
|
||||
)
|
||||
|
||||
LLM_ROUTER_URL = os.getenv("LLM_ROUTER_URL", "http://strikepackage-llm-router:8000")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Hello World"}
|
||||
443
services/hackgpt-api/app/llm_help.py
Normal file
@@ -0,0 +1,443 @@
|
||||
"""
|
||||
LLM Help Module
|
||||
Provides LLM-powered assistance including chat help, autocomplete, and config suggestions.
|
||||
Maintains conversation context for persistent help sessions.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
import os
|
||||
import httpx
|
||||
import json
|
||||
|
||||
|
||||
# Store conversation history per session
|
||||
# Note: In production, use Redis or similar with TTL for scalability
|
||||
# This simple in-memory dict will grow unbounded - implement cleanup as needed
|
||||
conversation_contexts: Dict[str, List[Dict[str, str]]] = {}
|
||||
MAX_SESSIONS = 1000 # Limit number of concurrent sessions
|
||||
|
||||
|
||||
async def chat_completion(
|
||||
message: str,
|
||||
session_id: Optional[str] = None,
|
||||
context: Optional[str] = None,
|
||||
provider: str = "ollama",
|
||||
model: str = "llama3.2",
|
||||
system_prompt: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get LLM chat completion with context awareness.
|
||||
|
||||
Args:
|
||||
message: User message
|
||||
session_id: Session ID for maintaining conversation context
|
||||
context: Additional context about current page/operation
|
||||
provider: LLM provider (ollama, openai, anthropic)
|
||||
model: Model name
|
||||
system_prompt: Custom system prompt (uses default if not provided)
|
||||
|
||||
Returns:
|
||||
Dictionary with LLM response and metadata
|
||||
"""
|
||||
# Default system prompt for help
|
||||
if not system_prompt:
|
||||
system_prompt = """You are a helpful AI assistant for StrikePackageGPT, a security testing platform.
|
||||
You help users with:
|
||||
- Understanding security tools and concepts
|
||||
- Writing and understanding nmap, nikto, and other security tool commands
|
||||
- Interpreting scan results and vulnerabilities
|
||||
- Best practices for penetration testing
|
||||
- Navigation and usage of the platform
|
||||
|
||||
Provide clear, concise, and actionable advice. Include command examples when relevant.
|
||||
Always emphasize ethical hacking practices and legal considerations."""
|
||||
|
||||
# Build messages with conversation history
|
||||
messages = [{"role": "system", "content": system_prompt}]
|
||||
|
||||
# Add conversation history if session_id provided
|
||||
if session_id and session_id in conversation_contexts:
|
||||
messages.extend(conversation_contexts[session_id][-10:]) # Last 10 messages
|
||||
|
||||
# Add context if provided
|
||||
if context:
|
||||
messages.append({"role": "system", "content": f"Current context: {context}"})
|
||||
|
||||
# Add user message
|
||||
messages.append({"role": "user", "content": message})
|
||||
|
||||
# Get LLM response
|
||||
try:
|
||||
llm_router_url = os.getenv("LLM_ROUTER_URL", "http://strikepackage-llm-router:8000")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{llm_router_url}/chat",
|
||||
json={
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 2048
|
||||
},
|
||||
timeout=120.0
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
assistant_message = result.get("content", "")
|
||||
|
||||
# Store in conversation history
|
||||
if session_id:
|
||||
if session_id not in conversation_contexts:
|
||||
# Cleanup old sessions if limit reached
|
||||
if len(conversation_contexts) >= MAX_SESSIONS:
|
||||
# Remove oldest session (simple FIFO)
|
||||
oldest_session = next(iter(conversation_contexts))
|
||||
del conversation_contexts[oldest_session]
|
||||
conversation_contexts[session_id] = []
|
||||
conversation_contexts[session_id].append({"role": "user", "content": message})
|
||||
conversation_contexts[session_id].append({"role": "assistant", "content": assistant_message})
|
||||
|
||||
return {
|
||||
"message": assistant_message,
|
||||
"session_id": session_id,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"success": True
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"message": "I'm having trouble connecting to the LLM service. Please try again.",
|
||||
"error": response.text,
|
||||
"success": False
|
||||
}
|
||||
|
||||
except httpx.ConnectError:
|
||||
return {
|
||||
"message": "LLM service is not available. Please check your connection.",
|
||||
"error": "Connection failed",
|
||||
"success": False
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"message": "An error occurred while processing your request.",
|
||||
"error": str(e),
|
||||
"success": False
|
||||
}
|
||||
|
||||
|
||||
async def get_autocomplete(
|
||||
partial_text: str,
|
||||
context_type: str = "command",
|
||||
max_suggestions: int = 5
|
||||
) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Get autocomplete suggestions for commands or configurations.
|
||||
|
||||
Args:
|
||||
partial_text: Partial text entered by user
|
||||
context_type: Type of autocomplete (command, config, target)
|
||||
max_suggestions: Maximum number of suggestions to return
|
||||
|
||||
Returns:
|
||||
List of suggestion dictionaries with text and description
|
||||
"""
|
||||
suggestions = []
|
||||
|
||||
if context_type == "command":
|
||||
suggestions = _get_command_suggestions(partial_text)
|
||||
elif context_type == "config":
|
||||
suggestions = _get_config_suggestions(partial_text)
|
||||
elif context_type == "target":
|
||||
suggestions = _get_target_suggestions(partial_text)
|
||||
|
||||
return suggestions[:max_suggestions]
|
||||
|
||||
|
||||
def _get_command_suggestions(partial_text: str) -> List[Dict[str, str]]:
|
||||
"""Get command autocomplete suggestions."""
|
||||
# Common security tool commands
|
||||
commands = [
|
||||
{"text": "nmap -sV -sC", "description": "Service version detection with default scripts"},
|
||||
{"text": "nmap -p- -T4", "description": "Scan all ports with aggressive timing"},
|
||||
{"text": "nmap -sS -O", "description": "SYN stealth scan with OS detection"},
|
||||
{"text": "nmap --script vuln", "description": "Run vulnerability detection scripts"},
|
||||
{"text": "nikto -h", "description": "Web server vulnerability scan"},
|
||||
{"text": "gobuster dir -u", "description": "Directory brute-forcing"},
|
||||
{"text": "sqlmap -u", "description": "SQL injection testing"},
|
||||
{"text": "whatweb", "description": "Web technology fingerprinting"},
|
||||
{"text": "searchsploit", "description": "Search exploit database"},
|
||||
{"text": "hydra -l", "description": "Network login cracking"}
|
||||
]
|
||||
|
||||
# Filter based on partial text
|
||||
partial_lower = partial_text.lower()
|
||||
return [cmd for cmd in commands if cmd["text"].lower().startswith(partial_lower)]
|
||||
|
||||
|
||||
def _get_config_suggestions(partial_text: str) -> List[Dict[str, str]]:
|
||||
"""Get configuration autocomplete suggestions."""
|
||||
configs = [
|
||||
{"text": "timeout", "description": "Command execution timeout in seconds"},
|
||||
{"text": "max_workers", "description": "Maximum parallel workers"},
|
||||
{"text": "scan_intensity", "description": "Scan aggressiveness (1-5)"},
|
||||
{"text": "rate_limit", "description": "Requests per second limit"},
|
||||
{"text": "default_ports", "description": "Default ports to scan"},
|
||||
{"text": "output_format", "description": "Output format (json, xml, text)"},
|
||||
{"text": "log_level", "description": "Logging verbosity (debug, info, warning, error)"},
|
||||
{"text": "retry_count", "description": "Number of retries on failure"}
|
||||
]
|
||||
|
||||
partial_lower = partial_text.lower()
|
||||
return [cfg for cfg in configs if cfg["text"].lower().startswith(partial_lower)]
|
||||
|
||||
|
||||
def _get_target_suggestions(partial_text: str) -> List[Dict[str, str]]:
|
||||
"""Get target specification autocomplete suggestions."""
|
||||
suggestions = [
|
||||
{"text": "192.168.1.0/24", "description": "Scan entire /24 subnet"},
|
||||
{"text": "192.168.1.1-50", "description": "Scan IP range"},
|
||||
{"text": "10.0.0.0/8", "description": "Scan entire /8 network"},
|
||||
{"text": "localhost", "description": "Scan local machine"},
|
||||
{"text": "example.com", "description": "Scan domain name"}
|
||||
]
|
||||
|
||||
return suggestions
|
||||
|
||||
|
||||
async def explain_anything(
|
||||
item: str,
|
||||
item_type: str = "auto",
|
||||
context: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Explain anything using LLM - commands, configs, errors, concepts.
|
||||
|
||||
Args:
|
||||
item: The item to explain
|
||||
item_type: Type of item (auto, command, config, error, concept)
|
||||
context: Additional context
|
||||
|
||||
Returns:
|
||||
Dictionary with explanation
|
||||
"""
|
||||
# Auto-detect type if not specified
|
||||
if item_type == "auto":
|
||||
item_type = _detect_item_type(item)
|
||||
|
||||
# Build appropriate prompt based on type
|
||||
prompts = {
|
||||
"command": f"Explain this security command in plain English:\n{item}\n\nInclude: what it does, any flags/options, expected output, and safety considerations.",
|
||||
"config": f"Explain this configuration setting:\n{item}\n\nInclude: purpose, typical values, and recommendations.",
|
||||
"error": f"Explain this error message:\n{item}\n\nInclude: what went wrong, likely causes, and how to fix it.",
|
||||
"concept": f"Explain this security concept:\n{item}\n\nProvide a clear, beginner-friendly explanation with examples.",
|
||||
"scan_result": f"Explain this scan result:\n{item}\n\nInclude: significance, risk level, and recommended actions."
|
||||
}
|
||||
|
||||
prompt = prompts.get(item_type, f"Explain: {item}")
|
||||
|
||||
# Get explanation from LLM
|
||||
result = await chat_completion(
|
||||
message=prompt,
|
||||
system_prompt="You are a security education assistant. Provide clear, concise explanations suitable for both beginners and experts. Use plain English and include practical examples."
|
||||
)
|
||||
|
||||
return {
|
||||
"item": item,
|
||||
"item_type": item_type,
|
||||
"explanation": result.get("message", ""),
|
||||
"success": result.get("success", False)
|
||||
}
|
||||
|
||||
|
||||
def _detect_item_type(item: str) -> str:
|
||||
"""Detect what type of item is being explained."""
|
||||
item_lower = item.lower()
|
||||
|
||||
# Check for command patterns
|
||||
if any(tool in item_lower for tool in ['nmap', 'nikto', 'gobuster', 'sqlmap', 'hydra']):
|
||||
return "command"
|
||||
|
||||
# Check for error patterns
|
||||
if any(word in item_lower for word in ['error', 'exception', 'failed', 'denied']):
|
||||
return "error"
|
||||
|
||||
# Check for config patterns
|
||||
if '=' in item or ':' in item or 'config' in item_lower:
|
||||
return "config"
|
||||
|
||||
# Check for scan result patterns
|
||||
if any(word in item_lower for word in ['open', 'closed', 'filtered', 'vulnerability', 'port']):
|
||||
return "scan_result"
|
||||
|
||||
# Default to concept
|
||||
return "concept"
|
||||
|
||||
|
||||
async def suggest_config(
|
||||
config_type: str,
|
||||
current_values: Optional[Dict] = None,
|
||||
use_case: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get LLM-powered configuration suggestions.
|
||||
|
||||
Args:
|
||||
config_type: Type of configuration (scan, system, security)
|
||||
current_values: Current configuration values
|
||||
use_case: Specific use case or scenario
|
||||
|
||||
Returns:
|
||||
Dictionary with configuration suggestions
|
||||
"""
|
||||
prompt_parts = [f"Suggest optimal configuration for {config_type}."]
|
||||
|
||||
if current_values:
|
||||
prompt_parts.append(f"\nCurrent configuration:\n{json.dumps(current_values, indent=2)}")
|
||||
|
||||
if use_case:
|
||||
prompt_parts.append(f"\nUse case: {use_case}")
|
||||
|
||||
prompt_parts.append("\nProvide recommended values with explanations. Format as JSON if possible.")
|
||||
|
||||
result = await chat_completion(
|
||||
message="\n".join(prompt_parts),
|
||||
system_prompt="You are a security configuration expert. Provide optimal, secure, and practical configuration recommendations."
|
||||
)
|
||||
|
||||
# Try to extract JSON from response
|
||||
response_text = result.get("message", "")
|
||||
suggested_config = _extract_json_from_text(response_text)
|
||||
|
||||
return {
|
||||
"config_type": config_type,
|
||||
"suggestions": suggested_config or {},
|
||||
"explanation": response_text,
|
||||
"success": result.get("success", False)
|
||||
}
|
||||
|
||||
|
||||
def _extract_json_from_text(text: str) -> Optional[Dict]:
|
||||
"""Try to extract JSON object from text."""
|
||||
try:
|
||||
# Look for JSON object in text
|
||||
start = text.find('{')
|
||||
end = text.rfind('}')
|
||||
if start != -1 and end != -1:
|
||||
json_str = text[start:end+1]
|
||||
return json.loads(json_str)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
async def get_step_by_step(
|
||||
task: str,
|
||||
skill_level: str = "intermediate"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get step-by-step instructions for a task.
|
||||
|
||||
Args:
|
||||
task: The task to get instructions for
|
||||
skill_level: User skill level (beginner, intermediate, advanced)
|
||||
|
||||
Returns:
|
||||
Dictionary with step-by-step instructions
|
||||
"""
|
||||
skill_context = {
|
||||
"beginner": "Explain in simple terms, avoid jargon, include screenshots references",
|
||||
"intermediate": "Provide clear steps with command examples",
|
||||
"advanced": "Be concise, focus on efficiency and best practices"
|
||||
}
|
||||
|
||||
context = skill_context.get(skill_level, skill_context["intermediate"])
|
||||
|
||||
prompt = f"""Provide step-by-step instructions for: {task}
|
||||
|
||||
User skill level: {skill_level}
|
||||
{context}
|
||||
|
||||
Format as numbered steps with clear actions. Include any commands to run."""
|
||||
|
||||
result = await chat_completion(
|
||||
message=prompt,
|
||||
system_prompt="You are an expert security instructor. Provide clear, actionable step-by-step guidance."
|
||||
)
|
||||
|
||||
# Parse steps from response
|
||||
steps = _parse_steps_from_text(result.get("message", ""))
|
||||
|
||||
return {
|
||||
"task": task,
|
||||
"skill_level": skill_level,
|
||||
"steps": steps,
|
||||
"full_explanation": result.get("message", ""),
|
||||
"success": result.get("success", False)
|
||||
}
|
||||
|
||||
|
||||
def _parse_steps_from_text(text: str) -> List[Dict[str, str]]:
|
||||
"""Parse numbered steps from text."""
|
||||
steps = []
|
||||
lines = text.split('\n')
|
||||
|
||||
for line in lines:
|
||||
# Match patterns like "1.", "Step 1:", "1)"
|
||||
import re
|
||||
match = re.match(r'^(?:Step\s+)?(\d+)[.):]\s*(.+)$', line.strip(), re.IGNORECASE)
|
||||
if match:
|
||||
step_num = int(match.group(1))
|
||||
step_text = match.group(2).strip()
|
||||
steps.append({
|
||||
"number": step_num,
|
||||
"instruction": step_text
|
||||
})
|
||||
|
||||
return steps
|
||||
|
||||
|
||||
def clear_conversation_context(session_id: str) -> bool:
|
||||
"""
|
||||
Clear conversation context for a session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID to clear
|
||||
|
||||
Returns:
|
||||
True if cleared, False if session didn't exist
|
||||
"""
|
||||
if session_id in conversation_contexts:
|
||||
del conversation_contexts[session_id]
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_conversation_summary(session_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get summary of conversation for a session.
|
||||
|
||||
Args:
|
||||
session_id: Session ID
|
||||
|
||||
Returns:
|
||||
Dictionary with conversation summary
|
||||
"""
|
||||
if session_id not in conversation_contexts:
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"exists": False,
|
||||
"message_count": 0
|
||||
}
|
||||
|
||||
messages = conversation_contexts[session_id]
|
||||
user_messages = [m for m in messages if m["role"] == "user"]
|
||||
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"exists": True,
|
||||
"message_count": len(messages),
|
||||
"user_message_count": len(user_messages),
|
||||
"last_messages": messages[-5:] if messages else []
|
||||
}
|
||||
@@ -32,10 +32,18 @@ app.add_middleware(
|
||||
LLM_ROUTER_URL = os.getenv("LLM_ROUTER_URL", "http://strikepackage-llm-router:8000")
|
||||
KALI_EXECUTOR_URL = os.getenv("KALI_EXECUTOR_URL", "http://strikepackage-kali-executor:8002")
|
||||
|
||||
# Default LLM Configuration (can be overridden via environment or API)
|
||||
DEFAULT_LLM_PROVIDER = os.getenv("DEFAULT_LLM_PROVIDER", "ollama")
|
||||
DEFAULT_LLM_MODEL = os.getenv("DEFAULT_LLM_MODEL", "llama3.1:latest")
|
||||
|
||||
# In-memory storage (use Redis in production)
|
||||
tasks: Dict[str, Any] = {}
|
||||
sessions: Dict[str, Dict] = {}
|
||||
scan_results: Dict[str, Any] = {}
|
||||
llm_preferences: Dict[str, Any] = {
|
||||
"provider": DEFAULT_LLM_PROVIDER,
|
||||
"model": DEFAULT_LLM_MODEL
|
||||
}
|
||||
|
||||
|
||||
# ============== Models ==============
|
||||
@@ -50,22 +58,27 @@ class ChatRequest(BaseModel):
|
||||
message: str
|
||||
session_id: Optional[str] = None
|
||||
context: Optional[str] = None
|
||||
provider: str = "ollama"
|
||||
model: str = "llama3.2"
|
||||
provider: Optional[str] = None # None means use default
|
||||
model: Optional[str] = None # None means use default
|
||||
|
||||
|
||||
class PhaseChatRequest(BaseModel):
|
||||
message: str
|
||||
phase: str
|
||||
provider: str = "ollama"
|
||||
model: str = "llama3.2"
|
||||
provider: Optional[str] = None # None means use default
|
||||
model: Optional[str] = None # None means use default
|
||||
findings: List[Dict[str, Any]] = []
|
||||
|
||||
|
||||
class AttackChainRequest(BaseModel):
|
||||
findings: List[Dict[str, Any]]
|
||||
provider: str = "ollama"
|
||||
model: str = "llama3.2"
|
||||
provider: Optional[str] = None # None means use default
|
||||
model: Optional[str] = None # None means use default
|
||||
|
||||
|
||||
class LLMPreferencesRequest(BaseModel):
|
||||
provider: str
|
||||
model: str
|
||||
|
||||
|
||||
class CommandRequest(BaseModel):
|
||||
@@ -335,7 +348,7 @@ async def health_check():
|
||||
|
||||
@app.post("/chat")
|
||||
async def security_chat(request: ChatRequest):
|
||||
"""Chat with security-focused AI assistant"""
|
||||
"""Chat with security-focused AI assistant - uses default LLM preferences if not specified"""
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
@@ -357,8 +370,8 @@ vulnerabilities and defenses."""
|
||||
response = await client.post(
|
||||
f"{LLM_ROUTER_URL}/chat",
|
||||
json={
|
||||
"provider": request.provider,
|
||||
"model": request.model,
|
||||
"provider": request.provider or llm_preferences["provider"],
|
||||
"model": request.model or llm_preferences["model"],
|
||||
"messages": messages,
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 2048
|
||||
@@ -376,7 +389,7 @@ vulnerabilities and defenses."""
|
||||
|
||||
@app.post("/chat/phase")
|
||||
async def phase_aware_chat(request: PhaseChatRequest):
|
||||
"""Phase-aware chat with context from current pentest phase"""
|
||||
"""Phase-aware chat with context from current pentest phase - uses default LLM preferences if not specified"""
|
||||
phase_prompt = PHASE_PROMPTS.get(request.phase, PHASE_PROMPTS["recon"])
|
||||
|
||||
# Build context from findings if available
|
||||
@@ -400,8 +413,8 @@ async def phase_aware_chat(request: PhaseChatRequest):
|
||||
response = await client.post(
|
||||
f"{LLM_ROUTER_URL}/chat",
|
||||
json={
|
||||
"provider": request.provider,
|
||||
"model": request.model,
|
||||
"provider": request.provider or llm_preferences["provider"],
|
||||
"model": request.model or llm_preferences["model"],
|
||||
"messages": messages,
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 2048
|
||||
@@ -515,8 +528,8 @@ Only return valid JSON."""
|
||||
response = await client.post(
|
||||
f"{LLM_ROUTER_URL}/chat",
|
||||
json={
|
||||
"provider": request.provider,
|
||||
"model": request.model,
|
||||
"provider": request.provider or llm_preferences["provider"],
|
||||
"model": request.model or llm_preferences["model"],
|
||||
"messages": messages,
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 2048
|
||||
@@ -729,6 +742,85 @@ async def clear_scans():
|
||||
return {"status": "cleared", "message": "All scan history cleared"}
|
||||
|
||||
|
||||
# ============== Interactive Command Capture ==============
|
||||
|
||||
@app.get("/commands/captured")
|
||||
async def get_captured_commands(limit: int = 50, since: Optional[str] = None):
|
||||
"""
|
||||
Get commands that were run directly in the Kali container.
|
||||
These are captured via the command logging system in interactive shells.
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{KALI_EXECUTOR_URL}/captured_commands",
|
||||
params={"limit": limit, "since": since} if since else {"limit": limit},
|
||||
timeout=10.0
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return {"commands": [], "error": "Could not retrieve captured commands"}
|
||||
|
||||
captured = response.json()
|
||||
commands = captured.get("commands", [])
|
||||
|
||||
# Import captured commands into scan_results for unified history
|
||||
for cmd in commands:
|
||||
cmd_id = cmd.get("command_id")
|
||||
if cmd_id and cmd_id not in scan_results:
|
||||
scan_results[cmd_id] = {
|
||||
"scan_id": cmd_id,
|
||||
"tool": cmd.get("command", "").split()[0] if cmd.get("command") else "unknown",
|
||||
"target": "interactive",
|
||||
"scan_type": "manual",
|
||||
"command": cmd.get("command"),
|
||||
"status": cmd.get("status", "completed"),
|
||||
"started_at": cmd.get("timestamp"),
|
||||
"completed_at": cmd.get("completed_at"),
|
||||
"result": {
|
||||
"stdout": cmd.get("stdout", ""),
|
||||
"stderr": cmd.get("stderr", ""),
|
||||
"exit_code": cmd.get("exit_code"),
|
||||
"duration": cmd.get("duration")
|
||||
},
|
||||
"source": cmd.get("source", "interactive_shell"),
|
||||
"user": cmd.get("user"),
|
||||
"working_dir": cmd.get("working_dir")
|
||||
}
|
||||
|
||||
# Parse output if available
|
||||
if cmd.get("stdout"):
|
||||
tool = cmd.get("command", "").split()[0]
|
||||
parsed = parse_tool_output(tool, cmd.get("stdout", ""))
|
||||
scan_results[cmd_id]["parsed"] = parsed
|
||||
|
||||
return {
|
||||
"commands": commands,
|
||||
"count": len(commands),
|
||||
"imported_to_history": True,
|
||||
"message": "Captured commands are now visible in scan history"
|
||||
}
|
||||
|
||||
except httpx.ConnectError:
|
||||
return {"commands": [], "error": "Kali executor service not available"}
|
||||
except Exception as e:
|
||||
return {"commands": [], "error": str(e)}
|
||||
|
||||
|
||||
@app.post("/commands/sync")
|
||||
async def sync_captured_commands():
|
||||
"""
|
||||
Sync all captured commands from the Kali container into the unified scan history.
|
||||
This allows commands run directly in the container to appear in the dashboard.
|
||||
"""
|
||||
result = await get_captured_commands(limit=1000)
|
||||
return {
|
||||
"status": "synced",
|
||||
"imported_count": result.get("count", 0),
|
||||
"message": "All captured commands are now visible in dashboard history"
|
||||
}
|
||||
|
||||
|
||||
# ============== Output Parsing ==============
|
||||
|
||||
def parse_tool_output(tool: str, output: str) -> Dict[str, Any]:
|
||||
@@ -840,7 +932,7 @@ def parse_gobuster_output(output: str) -> Dict[str, Any]:
|
||||
|
||||
@app.post("/ai-scan")
|
||||
async def ai_assisted_scan(request: ChatRequest, background_tasks: BackgroundTasks):
|
||||
"""Use AI to determine and run appropriate scan."""
|
||||
"""Use AI to determine and run appropriate scan - uses default LLM preferences if not specified"""
|
||||
# Get AI suggestion
|
||||
messages = [
|
||||
{"role": "system", "content": SECURITY_PROMPTS["command_assist"]},
|
||||
@@ -852,8 +944,8 @@ async def ai_assisted_scan(request: ChatRequest, background_tasks: BackgroundTas
|
||||
response = await client.post(
|
||||
f"{LLM_ROUTER_URL}/chat",
|
||||
json={
|
||||
"provider": request.provider,
|
||||
"model": request.model,
|
||||
"provider": request.provider or llm_preferences["provider"],
|
||||
"model": request.model or llm_preferences["model"],
|
||||
"messages": messages,
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 1024
|
||||
@@ -916,8 +1008,8 @@ async def run_analysis(task_id: str, request: SecurityAnalysisRequest):
|
||||
response = await client.post(
|
||||
f"{LLM_ROUTER_URL}/chat",
|
||||
json={
|
||||
"provider": "ollama",
|
||||
"model": "llama3.2",
|
||||
"provider": llm_preferences["provider"],
|
||||
"model": llm_preferences["model"],
|
||||
"messages": [
|
||||
{"role": "system", "content": prompt},
|
||||
{"role": "user", "content": f"Analyze target: {request.target}\nOptions: {request.options}"}
|
||||
@@ -981,7 +1073,7 @@ async def list_tools():
|
||||
|
||||
@app.post("/suggest-command")
|
||||
async def suggest_command(request: ChatRequest):
|
||||
"""Get AI-suggested security commands based on context"""
|
||||
"""Get AI-suggested security commands based on context - uses default LLM preferences if not specified"""
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
@@ -1004,8 +1096,8 @@ Only suggest commands for legitimate security testing purposes."""
|
||||
response = await client.post(
|
||||
f"{LLM_ROUTER_URL}/chat",
|
||||
json={
|
||||
"provider": request.provider,
|
||||
"model": request.model,
|
||||
"provider": request.provider or llm_preferences["provider"],
|
||||
"model": request.model or llm_preferences["model"],
|
||||
"messages": messages,
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 1024
|
||||
@@ -1021,6 +1113,378 @@ Only suggest commands for legitimate security testing purposes."""
|
||||
raise HTTPException(status_code=503, detail="LLM Router service not available")
|
||||
|
||||
|
||||
# ============== Nmap Parser Endpoints ==============
|
||||
|
||||
@app.post("/api/nmap/parse")
|
||||
async def parse_nmap(format: str = "xml", content: str = ""):
|
||||
"""Parse Nmap output (XML or JSON)"""
|
||||
try:
|
||||
from . import nmap_parser
|
||||
|
||||
if format == "xml":
|
||||
hosts = nmap_parser.parse_nmap_xml(content)
|
||||
elif format == "json":
|
||||
hosts = nmap_parser.parse_nmap_json(content)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Format must be 'xml' or 'json'")
|
||||
|
||||
return {"hosts": hosts, "count": len(hosts)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Parse error: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/api/nmap/hosts")
|
||||
async def get_nmap_hosts(scan_id: Optional[str] = None):
|
||||
"""Get parsed host data for network map"""
|
||||
# This could be extended to fetch from a database based on scan_id
|
||||
# For now, return from the scan_results if available
|
||||
if scan_id and scan_id in scan_results:
|
||||
result = scan_results[scan_id]
|
||||
hosts = result.get("parsed", {}).get("hosts", [])
|
||||
return {"hosts": hosts}
|
||||
|
||||
return {"hosts": [], "message": "No scan data available"}
|
||||
|
||||
|
||||
# ============== Voice Control Endpoints ==============
|
||||
|
||||
@app.post("/api/voice/transcribe")
|
||||
async def transcribe_audio(audio_data: Optional[bytes] = None):
|
||||
"""Transcribe audio to text using Whisper"""
|
||||
if not audio_data:
|
||||
raise HTTPException(status_code=400, detail="No audio data provided")
|
||||
|
||||
try:
|
||||
from . import voice
|
||||
result = voice.transcribe_audio(audio_data)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Transcription error: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/api/voice/speak")
|
||||
async def text_to_speech(text: str, voice_name: str = "alloy"):
|
||||
"""Convert text to speech"""
|
||||
try:
|
||||
from . import voice as voice_module
|
||||
audio_bytes = voice_module.speak_text(text, voice=voice_name)
|
||||
|
||||
if audio_bytes:
|
||||
from fastapi.responses import Response
|
||||
return Response(content=audio_bytes, media_type="audio/mp3")
|
||||
else:
|
||||
return {"message": "TTS not available, use browser fallback"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"TTS error: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/api/voice/command")
|
||||
async def process_voice_command(text: str):
|
||||
"""Parse and route voice command"""
|
||||
try:
|
||||
from . import voice as voice_module
|
||||
|
||||
# Parse command
|
||||
command_result = voice_module.parse_voice_command(text)
|
||||
|
||||
# Route command
|
||||
routing_info = voice_module.route_command(command_result)
|
||||
|
||||
return {
|
||||
"command": command_result,
|
||||
"routing": routing_info,
|
||||
"speak_response": routing_info.get("message", "")
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Command processing error: {str(e)}")
|
||||
|
||||
|
||||
# ============== Explanation Endpoints ==============
|
||||
|
||||
@app.post("/api/explain")
|
||||
async def explain_item(
|
||||
type: str,
|
||||
content: str,
|
||||
context: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""Get explanation for config, log, error, etc."""
|
||||
try:
|
||||
from . import explain
|
||||
|
||||
if type == "config":
|
||||
result = explain.explain_config(content, content, context)
|
||||
elif type == "error":
|
||||
result = explain.explain_error(content, context=context)
|
||||
elif type == "log":
|
||||
log_level = context.get("level") if context else None
|
||||
result = explain.explain_log_entry(content, log_level)
|
||||
else:
|
||||
result = {"error": "Unknown explanation type"}
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Explanation error: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/api/wizard/help")
|
||||
async def get_wizard_help(type: str, step: int):
|
||||
"""Get help for wizard step"""
|
||||
try:
|
||||
from . import explain
|
||||
result = explain.get_wizard_step_help(type, step)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Help error: {str(e)}")
|
||||
|
||||
|
||||
# ============== LLM Help Endpoints ==============
|
||||
|
||||
@app.post("/api/llm/chat")
|
||||
async def llm_chat_help(
|
||||
message: str,
|
||||
session_id: Optional[str] = None,
|
||||
context: Optional[str] = None,
|
||||
provider: Optional[str] = None,
|
||||
model: Optional[str] = None
|
||||
):
|
||||
"""LLM-powered chat help - uses default preferences if provider/model not specified"""
|
||||
try:
|
||||
from . import llm_help
|
||||
result = await llm_help.chat_completion(
|
||||
message=message,
|
||||
session_id=session_id,
|
||||
context=context,
|
||||
provider=provider or llm_preferences["provider"],
|
||||
model=model or llm_preferences["model"]
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Chat error: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/api/llm/autocomplete")
|
||||
async def get_autocomplete(
|
||||
partial_text: str,
|
||||
context_type: str = "command",
|
||||
max_suggestions: int = 5
|
||||
):
|
||||
"""Get autocomplete suggestions"""
|
||||
try:
|
||||
from . import llm_help
|
||||
suggestions = await llm_help.get_autocomplete(
|
||||
partial_text=partial_text,
|
||||
context_type=context_type,
|
||||
max_suggestions=max_suggestions
|
||||
)
|
||||
return {"suggestions": suggestions}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Autocomplete error: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/api/llm/explain")
|
||||
async def llm_explain(
|
||||
item: str,
|
||||
item_type: str = "auto",
|
||||
context: Optional[Dict] = None
|
||||
):
|
||||
"""LLM-powered explanation"""
|
||||
try:
|
||||
from . import llm_help
|
||||
result = await llm_help.explain_anything(
|
||||
item=item,
|
||||
item_type=item_type,
|
||||
context=context
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Explanation error: {str(e)}")
|
||||
|
||||
|
||||
# ============== Config Validation Endpoints ==============
|
||||
|
||||
@app.post("/api/config/validate")
|
||||
async def validate_configuration(
|
||||
config_data: Dict[str, Any],
|
||||
config_type: str = "general"
|
||||
):
|
||||
"""Validate configuration"""
|
||||
try:
|
||||
from . import config_validator
|
||||
result = config_validator.validate_config(config_data, config_type)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Validation error: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/api/config/backup")
|
||||
async def backup_configuration(
|
||||
config_name: str,
|
||||
config_data: Dict[str, Any],
|
||||
description: str = ""
|
||||
):
|
||||
"""Create configuration backup"""
|
||||
try:
|
||||
from . import config_validator
|
||||
result = config_validator.backup_config(config_name, config_data, description)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Backup error: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/api/config/restore")
|
||||
async def restore_configuration(backup_id: str):
|
||||
"""Restore configuration from backup"""
|
||||
try:
|
||||
from . import config_validator
|
||||
result = config_validator.restore_config(backup_id)
|
||||
if not result.get("success"):
|
||||
raise HTTPException(status_code=404, detail=result.get("error"))
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Restore error: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/api/config/backups")
|
||||
async def list_configuration_backups(config_name: Optional[str] = None):
|
||||
"""List available backups"""
|
||||
try:
|
||||
from . import config_validator
|
||||
result = config_validator.list_backups(config_name)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"List backups error: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/api/config/autofix")
|
||||
async def autofix_configuration(
|
||||
validation_result: Dict[str, Any],
|
||||
config_data: Dict[str, Any]
|
||||
):
|
||||
"""Suggest automatic fixes for configuration"""
|
||||
try:
|
||||
from . import config_validator
|
||||
result = config_validator.suggest_autofix(validation_result, config_data)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Autofix error: {str(e)}")
|
||||
|
||||
|
||||
# ============== Webhook & Integration Endpoints ==============
|
||||
|
||||
@app.post("/api/webhook/n8n")
|
||||
async def n8n_webhook(data: Dict[str, Any]):
|
||||
"""Receive webhook from n8n workflow"""
|
||||
try:
|
||||
# Process n8n webhook data
|
||||
# This could trigger scans, send notifications, etc.
|
||||
return {
|
||||
"status": "received",
|
||||
"data": data,
|
||||
"message": "Webhook processed successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Webhook error: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/api/alerts/push")
|
||||
async def send_push_notification(
|
||||
title: str,
|
||||
message: str,
|
||||
severity: str = "info"
|
||||
):
|
||||
"""Send push notification for critical alerts"""
|
||||
try:
|
||||
# This could integrate with services like:
|
||||
# - Pushover
|
||||
# - Slack
|
||||
# - Discord
|
||||
# - Email
|
||||
return {
|
||||
"status": "sent",
|
||||
"title": title,
|
||||
"message": message,
|
||||
"severity": severity
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Push notification error: {str(e)}")
|
||||
|
||||
|
||||
# ============== LLM Preferences ==============
|
||||
|
||||
@app.get("/api/llm/preferences")
|
||||
async def get_llm_preferences():
|
||||
"""
|
||||
Get current default LLM provider and model preferences.
|
||||
|
||||
Returns:
|
||||
Dictionary with provider, model, and available options
|
||||
"""
|
||||
try:
|
||||
# Get available providers from LLM router
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{LLM_ROUTER_URL}/providers", timeout=10.0)
|
||||
available_providers = response.json() if response.status_code == 200 else []
|
||||
|
||||
return {
|
||||
"current": {
|
||||
"provider": llm_preferences["provider"],
|
||||
"model": llm_preferences["model"]
|
||||
},
|
||||
"available_providers": available_providers,
|
||||
"description": "Current default LLM provider and model. These are used when no explicit provider/model is specified in API requests."
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"current": {
|
||||
"provider": llm_preferences["provider"],
|
||||
"model": llm_preferences["model"]
|
||||
},
|
||||
"available_providers": [],
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/llm/preferences")
|
||||
async def set_llm_preferences(request: LLMPreferencesRequest):
|
||||
"""
|
||||
Set default LLM provider and model preferences.
|
||||
|
||||
Args:
|
||||
request: LLMPreferencesRequest with provider and model
|
||||
|
||||
Returns:
|
||||
Updated preferences
|
||||
"""
|
||||
# Validate provider is available
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{LLM_ROUTER_URL}/providers", timeout=10.0)
|
||||
if response.status_code == 200:
|
||||
available_providers = response.json()
|
||||
provider_names = [p["name"] for p in available_providers]
|
||||
if request.provider not in provider_names:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Provider '{request.provider}' not available. Available: {provider_names}"
|
||||
)
|
||||
except httpx.ConnectError:
|
||||
# LLM router not available, proceed anyway
|
||||
pass
|
||||
|
||||
# Update preferences
|
||||
llm_preferences["provider"] = request.provider
|
||||
llm_preferences["model"] = request.model
|
||||
|
||||
return {
|
||||
"status": "updated",
|
||||
"provider": llm_preferences["provider"],
|
||||
"model": llm_preferences["model"],
|
||||
"message": f"Default LLM set to {request.provider}/{request.model}"
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
||||
505
services/hackgpt-api/app/nmap_parser.py
Normal file
@@ -0,0 +1,505 @@
|
||||
"""
|
||||
Nmap Parser Module
|
||||
Parses Nmap XML or JSON output to extract host information including:
|
||||
- IP addresses, hostnames
|
||||
- Operating system detection
|
||||
- Device type classification (workstation/server/appliance)
|
||||
- MAC vendor information
|
||||
- Open ports and services
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
from typing import Dict, List, Any, Optional
|
||||
import re
|
||||
|
||||
|
||||
def parse_nmap_xml(xml_content: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parse Nmap XML output and extract host information.
|
||||
|
||||
Args:
|
||||
xml_content: Raw XML string from nmap -oX output
|
||||
|
||||
Returns:
|
||||
List of host dictionaries with parsed information
|
||||
"""
|
||||
hosts = []
|
||||
|
||||
try:
|
||||
# Clean up XML content - remove any non-XML content before the declaration
|
||||
xml_start = xml_content.find('<?xml')
|
||||
if xml_start == -1:
|
||||
xml_start = xml_content.find('<nmaprun')
|
||||
if xml_start > 0:
|
||||
xml_content = xml_content[xml_start:]
|
||||
|
||||
root = ET.fromstring(xml_content)
|
||||
|
||||
for host_elem in root.findall('.//host'):
|
||||
# Check if host is up
|
||||
status = host_elem.find('status')
|
||||
if status is None or status.get('state') != 'up':
|
||||
continue
|
||||
|
||||
host = _parse_host_element(host_elem)
|
||||
if host.get('ip'):
|
||||
hosts.append(host)
|
||||
|
||||
except ET.ParseError as e:
|
||||
print(f"XML parsing error: {e}")
|
||||
# Return empty list on parse error
|
||||
return []
|
||||
|
||||
return hosts
|
||||
|
||||
|
||||
def parse_nmap_json(json_content: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Parse Nmap JSON output and extract host information.
|
||||
|
||||
Args:
|
||||
json_content: JSON string from nmap with JSON output
|
||||
|
||||
Returns:
|
||||
List of host dictionaries with parsed information
|
||||
"""
|
||||
hosts = []
|
||||
|
||||
try:
|
||||
data = json.loads(json_content)
|
||||
|
||||
# Handle different JSON structures
|
||||
if isinstance(data, list):
|
||||
scan_results = data
|
||||
elif isinstance(data, dict):
|
||||
# Try common JSON nmap output structures
|
||||
scan_results = data.get('hosts', data.get('scan', []))
|
||||
else:
|
||||
return []
|
||||
|
||||
for host_data in scan_results:
|
||||
host = _parse_host_json(host_data)
|
||||
if host.get('ip'):
|
||||
hosts.append(host)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"JSON parsing error: {e}")
|
||||
return []
|
||||
|
||||
return hosts
|
||||
|
||||
|
||||
def _parse_host_element(host_elem: ET.Element) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse an individual host XML element.
|
||||
|
||||
Args:
|
||||
host_elem: XML Element representing a single host
|
||||
|
||||
Returns:
|
||||
Dictionary with host information
|
||||
"""
|
||||
host = {
|
||||
'ip': '',
|
||||
'hostname': '',
|
||||
'mac': '',
|
||||
'vendor': '',
|
||||
'os_type': '',
|
||||
'os_details': '',
|
||||
'device_type': '',
|
||||
'ports': [],
|
||||
'os_accuracy': 0
|
||||
}
|
||||
|
||||
# Extract IP address
|
||||
addr = host_elem.find("address[@addrtype='ipv4']")
|
||||
if addr is not None:
|
||||
host['ip'] = addr.get('addr', '')
|
||||
|
||||
# Extract MAC address and vendor
|
||||
mac = host_elem.find("address[@addrtype='mac']")
|
||||
if mac is not None:
|
||||
host['mac'] = mac.get('addr', '')
|
||||
host['vendor'] = mac.get('vendor', '')
|
||||
|
||||
# Extract hostname
|
||||
hostname_elem = host_elem.find(".//hostname")
|
||||
if hostname_elem is not None:
|
||||
host['hostname'] = hostname_elem.get('name', '')
|
||||
|
||||
# Extract OS information
|
||||
osmatch = host_elem.find(".//osmatch")
|
||||
if osmatch is not None:
|
||||
os_name = osmatch.get('name', '')
|
||||
host['os_details'] = os_name
|
||||
host['os_type'] = detect_os_type(os_name)
|
||||
try:
|
||||
host['os_accuracy'] = int(osmatch.get('accuracy', 0))
|
||||
except (ValueError, TypeError):
|
||||
host['os_accuracy'] = 0
|
||||
else:
|
||||
# Try osclass as fallback
|
||||
osclass = host_elem.find(".//osclass")
|
||||
if osclass is not None:
|
||||
osfamily = osclass.get('osfamily', '')
|
||||
osgen = osclass.get('osgen', '')
|
||||
host['os_type'] = detect_os_type(osfamily)
|
||||
host['os_details'] = f"{osfamily} {osgen}".strip()
|
||||
try:
|
||||
host['os_accuracy'] = int(osclass.get('accuracy', 0))
|
||||
except (ValueError, TypeError):
|
||||
host['os_accuracy'] = 0
|
||||
|
||||
# Extract ports
|
||||
for port_elem in host_elem.findall(".//port"):
|
||||
port_info = {
|
||||
'port': int(port_elem.get('portid', 0)),
|
||||
'protocol': port_elem.get('protocol', 'tcp'),
|
||||
'state': '',
|
||||
'service': '',
|
||||
'product': '',
|
||||
'version': ''
|
||||
}
|
||||
|
||||
state_elem = port_elem.find('state')
|
||||
if state_elem is not None:
|
||||
port_info['state'] = state_elem.get('state', '')
|
||||
|
||||
service_elem = port_elem.find('service')
|
||||
if service_elem is not None:
|
||||
port_info['service'] = service_elem.get('name', '')
|
||||
port_info['product'] = service_elem.get('product', '')
|
||||
port_info['version'] = service_elem.get('version', '')
|
||||
|
||||
# Use service info to help detect OS if not already detected
|
||||
if not host['os_type']:
|
||||
product = service_elem.get('product', '').lower()
|
||||
if 'microsoft' in product or 'windows' in product:
|
||||
host['os_type'] = 'Windows'
|
||||
elif 'apache' in product or 'nginx' in product or 'linux' in product:
|
||||
host['os_type'] = 'Linux'
|
||||
|
||||
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'])
|
||||
|
||||
# Classify device type
|
||||
host['device_type'] = classify_device_type(host)
|
||||
|
||||
return host
|
||||
|
||||
|
||||
def _parse_host_json(host_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse host data from JSON format.
|
||||
|
||||
Args:
|
||||
host_data: Dictionary containing host information
|
||||
|
||||
Returns:
|
||||
Standardized host dictionary
|
||||
"""
|
||||
host = {
|
||||
'ip': host_data.get('ip', host_data.get('address', '')),
|
||||
'hostname': host_data.get('hostname', host_data.get('name', '')),
|
||||
'mac': host_data.get('mac', ''),
|
||||
'vendor': host_data.get('vendor', ''),
|
||||
'os_type': '',
|
||||
'os_details': '',
|
||||
'device_type': '',
|
||||
'ports': [],
|
||||
'os_accuracy': 0
|
||||
}
|
||||
|
||||
# Extract OS information
|
||||
os_info = host_data.get('os', host_data.get('osmatch', {}))
|
||||
if isinstance(os_info, dict):
|
||||
host['os_details'] = os_info.get('name', os_info.get('details', ''))
|
||||
host['os_accuracy'] = int(os_info.get('accuracy', 0))
|
||||
elif isinstance(os_info, str):
|
||||
host['os_details'] = os_info
|
||||
|
||||
host['os_type'] = detect_os_type(host['os_details'])
|
||||
|
||||
# Extract ports
|
||||
ports_data = host_data.get('ports', host_data.get('tcp', {}))
|
||||
if isinstance(ports_data, list):
|
||||
host['ports'] = ports_data
|
||||
elif isinstance(ports_data, dict):
|
||||
for port_num, port_info in ports_data.items():
|
||||
if isinstance(port_info, dict):
|
||||
host['ports'].append({
|
||||
'port': int(port_num),
|
||||
'protocol': 'tcp',
|
||||
'state': port_info.get('state', ''),
|
||||
'service': port_info.get('service', port_info.get('name', '')),
|
||||
'product': port_info.get('product', ''),
|
||||
'version': port_info.get('version', '')
|
||||
})
|
||||
|
||||
# Infer OS from ports if unknown
|
||||
if not host['os_type'] and host['ports']:
|
||||
host['os_type'] = _infer_os_from_ports(host['ports'])
|
||||
|
||||
# Classify device type
|
||||
host['device_type'] = classify_device_type(host)
|
||||
|
||||
return host
|
||||
|
||||
|
||||
def detect_os_type(os_string: str) -> str:
|
||||
"""
|
||||
Detect OS type from an OS description string.
|
||||
|
||||
Args:
|
||||
os_string: OS description from nmap
|
||||
|
||||
Returns:
|
||||
Standardized OS type string
|
||||
"""
|
||||
if not os_string:
|
||||
return 'Unknown'
|
||||
|
||||
os_lower = os_string.lower()
|
||||
|
||||
# Windows detection
|
||||
if any(keyword in os_lower for keyword in ['windows', 'microsoft', 'win7', 'win10', 'win11', 'server 20']):
|
||||
return 'Windows'
|
||||
|
||||
# Linux detection
|
||||
elif any(keyword in os_lower for keyword in ['linux', 'ubuntu', 'debian', 'centos', 'red hat', 'rhel', 'fedora', 'arch', 'gentoo', 'suse']):
|
||||
return 'Linux'
|
||||
|
||||
# macOS detection
|
||||
elif any(keyword in os_lower for keyword in ['mac os', 'darwin', 'apple', 'macos']):
|
||||
return 'macOS'
|
||||
|
||||
# Unix variants
|
||||
elif any(keyword in os_lower for keyword in ['freebsd', 'openbsd', 'netbsd', 'unix', 'solaris', 'aix']):
|
||||
return 'Unix'
|
||||
|
||||
# Network devices
|
||||
elif any(keyword in os_lower for keyword in ['cisco', 'ios']):
|
||||
return 'Cisco'
|
||||
elif 'juniper' in os_lower or 'junos' in os_lower:
|
||||
return 'Juniper'
|
||||
elif 'fortinet' in os_lower or 'fortigate' in os_lower:
|
||||
return 'Fortinet'
|
||||
elif 'palo alto' in os_lower or 'panos' in os_lower:
|
||||
return 'Palo Alto'
|
||||
elif any(keyword in os_lower for keyword in ['switch', 'router', 'firewall', 'gateway']):
|
||||
return 'Network Device'
|
||||
|
||||
# Virtualization
|
||||
elif 'vmware' in os_lower or 'esxi' in os_lower:
|
||||
return 'VMware'
|
||||
elif 'hyper-v' in os_lower:
|
||||
return 'Hyper-V'
|
||||
|
||||
# Mobile
|
||||
elif 'android' in os_lower:
|
||||
return 'Android'
|
||||
elif 'ios' in os_lower and 'apple' in os_lower:
|
||||
return 'iOS'
|
||||
|
||||
# Printers and IoT
|
||||
elif any(keyword in os_lower for keyword in ['printer', 'hp jetdirect', 'canon', 'epson', 'xerox']):
|
||||
return 'Printer'
|
||||
elif 'iot' in os_lower or 'embedded' in os_lower:
|
||||
return 'IoT Device'
|
||||
|
||||
return 'Unknown'
|
||||
|
||||
|
||||
def classify_device_type(host: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Classify the device type based on OS, ports, and services.
|
||||
|
||||
Args:
|
||||
host: Host dictionary with OS and port information
|
||||
|
||||
Returns:
|
||||
Device type classification (workstation, server, network, appliance, etc.)
|
||||
"""
|
||||
os_type = host.get('os_type', '').lower()
|
||||
os_details = host.get('os_details', '').lower()
|
||||
ports = host.get('ports', [])
|
||||
vendor = host.get('vendor', '').lower()
|
||||
|
||||
port_numbers = {p['port'] for p in ports}
|
||||
services = {p.get('service', '').lower() for p in ports}
|
||||
|
||||
# Network infrastructure
|
||||
if os_type in ['cisco', 'juniper', 'fortinet', 'palo alto', 'network device']:
|
||||
if 'switch' in os_details or 'catalyst' in os_details:
|
||||
return 'Network Switch'
|
||||
elif 'router' in os_details or 'ios' in os_details:
|
||||
return 'Router'
|
||||
elif 'firewall' in os_details or 'fortigate' in os_details:
|
||||
return 'Firewall'
|
||||
else:
|
||||
return 'Network Device'
|
||||
|
||||
# Check for SNMP (common on network devices)
|
||||
if 161 in port_numbers or 162 in port_numbers:
|
||||
return 'Network Device'
|
||||
|
||||
# Printers
|
||||
if os_type == 'printer' or 9100 in port_numbers or 631 in port_numbers:
|
||||
return 'Printer'
|
||||
|
||||
# IoT devices
|
||||
if os_type == 'iot device':
|
||||
return 'IoT Device'
|
||||
|
||||
# Servers - check for common server ports and services
|
||||
server_indicators = {
|
||||
# Web servers
|
||||
80, 443, 8080, 8443,
|
||||
# Database servers
|
||||
3306, 5432, 1433, 27017, 6379,
|
||||
# Mail servers
|
||||
25, 587, 465, 110, 995, 143, 993,
|
||||
# File servers
|
||||
21, 22, 139, 445, 2049,
|
||||
# Directory services
|
||||
389, 636, 88, 464,
|
||||
# Application servers
|
||||
8000, 8001, 8888, 9000, 3000, 5000,
|
||||
# Virtualization
|
||||
902, 443
|
||||
}
|
||||
|
||||
server_services = {
|
||||
'http', 'https', 'apache', 'nginx', 'iis',
|
||||
'mysql', 'postgresql', 'mssql', 'mongodb', 'redis',
|
||||
'smtp', 'pop3', 'imap',
|
||||
'ftp', 'ssh', 'smb', 'nfs',
|
||||
'ldap', 'ldaps', 'kerberos',
|
||||
'vmware'
|
||||
}
|
||||
|
||||
# Check if it's explicitly a server OS
|
||||
if 'server' in os_details:
|
||||
return 'Server'
|
||||
|
||||
# Check for server ports/services
|
||||
if port_numbers & server_indicators or services & server_services:
|
||||
# More than 3 server ports suggests a server
|
||||
if len(port_numbers & server_indicators) >= 3:
|
||||
return 'Server'
|
||||
# Specific database or web server services
|
||||
if any(svc in services for svc in ['mysql', 'postgresql', 'mongodb', 'apache', 'nginx', 'iis']):
|
||||
return 'Server'
|
||||
|
||||
# Virtualization hosts
|
||||
if os_type in ['vmware', 'hyper-v'] or 'esxi' in os_details:
|
||||
return 'Virtualization Host'
|
||||
|
||||
# Workstations
|
||||
if os_type in ['windows', 'macos', 'linux']:
|
||||
# Windows/macOS are typically workstations unless server indicators
|
||||
if os_type in ['windows', 'macos']:
|
||||
if 3389 in port_numbers: # RDP
|
||||
# Could be either, but default to workstation
|
||||
return 'Workstation'
|
||||
return 'Workstation'
|
||||
# Linux could be either
|
||||
elif os_type == 'linux':
|
||||
# Desktop Linux if few ports open
|
||||
if len(port_numbers) <= 3:
|
||||
return 'Workstation'
|
||||
else:
|
||||
return 'Server'
|
||||
|
||||
# Mobile devices
|
||||
if os_type in ['android', 'ios']:
|
||||
return 'Mobile Device'
|
||||
|
||||
# Default classification
|
||||
if len(port_numbers) >= 5:
|
||||
return 'Server'
|
||||
elif len(port_numbers) >= 1:
|
||||
return 'Workstation'
|
||||
|
||||
return 'Unknown'
|
||||
|
||||
|
||||
def _infer_os_from_ports(ports: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
Infer OS type from open ports and services.
|
||||
|
||||
Args:
|
||||
ports: List of port dictionaries
|
||||
|
||||
Returns:
|
||||
Inferred OS type
|
||||
"""
|
||||
port_numbers = {p['port'] for p in ports}
|
||||
services = [p.get('service', '').lower() for p in ports]
|
||||
products = [p.get('product', '').lower() for p in ports]
|
||||
|
||||
# Windows indicators
|
||||
windows_ports = {135, 139, 445, 3389, 5985, 5986}
|
||||
if windows_ports & port_numbers:
|
||||
return 'Windows'
|
||||
|
||||
if any('microsoft' in p or 'windows' in p for p in products):
|
||||
return 'Windows'
|
||||
|
||||
# Linux indicators (SSH is common)
|
||||
if 22 in port_numbers and 'ssh' in services:
|
||||
# Could be Linux or Unix
|
||||
return 'Linux'
|
||||
|
||||
# Network device indicators
|
||||
if 161 in port_numbers or 162 in port_numbers: # SNMP
|
||||
return 'Network Device'
|
||||
if 23 in port_numbers: # Telnet (often network devices)
|
||||
return 'Network Device'
|
||||
|
||||
# Printer indicators
|
||||
if 9100 in port_numbers or 631 in port_numbers:
|
||||
return 'Printer'
|
||||
|
||||
return 'Unknown'
|
||||
|
||||
|
||||
def get_os_icon_name(host: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Get the appropriate icon name for a host based on OS and device type.
|
||||
|
||||
Args:
|
||||
host: Host dictionary
|
||||
|
||||
Returns:
|
||||
Icon filename (without extension)
|
||||
"""
|
||||
os_type = host.get('os_type', '').lower()
|
||||
device_type = host.get('device_type', '').lower()
|
||||
|
||||
# Device type takes precedence for specialized devices
|
||||
if 'server' in device_type:
|
||||
return 'server'
|
||||
elif 'network' in device_type or 'router' in device_type or 'switch' in device_type or 'firewall' in device_type:
|
||||
return 'network'
|
||||
elif 'printer' in device_type:
|
||||
return 'printer'
|
||||
elif 'workstation' in device_type:
|
||||
return 'workstation'
|
||||
|
||||
# Fall back to OS type
|
||||
if 'windows' in os_type:
|
||||
return 'windows'
|
||||
elif 'linux' in os_type or 'unix' in os_type:
|
||||
return 'linux'
|
||||
elif 'mac' in os_type:
|
||||
return 'mac'
|
||||
elif any(net in os_type for net in ['cisco', 'juniper', 'fortinet', 'network']):
|
||||
return 'network'
|
||||
|
||||
return 'unknown'
|
||||
508
services/hackgpt-api/app/voice.py
Normal file
@@ -0,0 +1,508 @@
|
||||
"""
|
||||
Voice Control Module
|
||||
Handles speech-to-text and text-to-speech functionality, plus voice command routing.
|
||||
Supports local Whisper (preferred) and OpenAI API as fallback.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
def transcribe_audio(audio_data: bytes, format: str = "wav") -> Dict[str, Any]:
|
||||
"""
|
||||
Transcribe audio to text using Whisper (local preferred) or OpenAI API.
|
||||
|
||||
Args:
|
||||
audio_data: Raw audio bytes
|
||||
format: Audio format (wav, mp3, webm, etc.)
|
||||
|
||||
Returns:
|
||||
Dictionary with transcription result and metadata
|
||||
{
|
||||
"text": "transcribed text",
|
||||
"language": "en",
|
||||
"confidence": 0.95,
|
||||
"method": "whisper-local" or "openai"
|
||||
}
|
||||
"""
|
||||
# Try local Whisper first
|
||||
try:
|
||||
return _transcribe_with_local_whisper(audio_data, format)
|
||||
except Exception as e:
|
||||
print(f"Local Whisper failed: {e}, falling back to OpenAI API")
|
||||
|
||||
# Fallback to OpenAI API if configured
|
||||
if os.getenv("OPENAI_API_KEY"):
|
||||
try:
|
||||
return _transcribe_with_openai(audio_data, format)
|
||||
except Exception as e:
|
||||
print(f"OpenAI transcription failed: {e}")
|
||||
return {
|
||||
"text": "",
|
||||
"error": f"Transcription failed: {str(e)}",
|
||||
"method": "none"
|
||||
}
|
||||
|
||||
return {
|
||||
"text": "",
|
||||
"error": "No transcription service available. Install Whisper or configure OPENAI_API_KEY.",
|
||||
"method": "none"
|
||||
}
|
||||
|
||||
|
||||
def _transcribe_with_local_whisper(audio_data: bytes, format: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Transcribe using local Whisper model.
|
||||
|
||||
Args:
|
||||
audio_data: Raw audio bytes
|
||||
format: Audio format
|
||||
|
||||
Returns:
|
||||
Transcription result dictionary
|
||||
"""
|
||||
try:
|
||||
import whisper
|
||||
|
||||
# Save audio to temporary file
|
||||
with tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) as temp_audio:
|
||||
temp_audio.write(audio_data)
|
||||
temp_audio_path = temp_audio.name
|
||||
|
||||
try:
|
||||
# Load model (use base model by default for speed/accuracy balance)
|
||||
model_size = os.getenv("WHISPER_MODEL", "base")
|
||||
model = whisper.load_model(model_size)
|
||||
|
||||
# Transcribe
|
||||
result = model.transcribe(temp_audio_path)
|
||||
|
||||
return {
|
||||
"text": result["text"].strip(),
|
||||
"language": result.get("language", "unknown"),
|
||||
"confidence": 1.0, # Whisper doesn't provide confidence scores
|
||||
"method": "whisper-local",
|
||||
"model": model_size
|
||||
}
|
||||
finally:
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.unlink(temp_audio_path)
|
||||
except (OSError, FileNotFoundError) as e:
|
||||
print(f"Warning: Could not delete temp file: {e}")
|
||||
|
||||
except ImportError:
|
||||
raise Exception("Whisper not installed. Install with: pip install openai-whisper")
|
||||
|
||||
|
||||
def _transcribe_with_openai(audio_data: bytes, format: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Transcribe using OpenAI Whisper API.
|
||||
|
||||
Args:
|
||||
audio_data: Raw audio bytes
|
||||
format: Audio format
|
||||
|
||||
Returns:
|
||||
Transcription result dictionary
|
||||
"""
|
||||
try:
|
||||
import httpx
|
||||
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
raise Exception("OPENAI_API_KEY not configured")
|
||||
|
||||
# Prepare multipart form data
|
||||
files = {
|
||||
'file': (f'audio.{format}', audio_data, f'audio/{format}')
|
||||
}
|
||||
data = {
|
||||
'model': 'whisper-1',
|
||||
'language': 'en' # Can be auto-detected by omitting this
|
||||
}
|
||||
|
||||
# Make API request
|
||||
with httpx.Client() as client:
|
||||
response = client.post(
|
||||
'https://api.openai.com/v1/audio/transcriptions',
|
||||
headers={'Authorization': f'Bearer {api_key}'},
|
||||
files=files,
|
||||
data=data,
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return {
|
||||
"text": result.get("text", "").strip(),
|
||||
"language": "en",
|
||||
"confidence": 1.0,
|
||||
"method": "openai"
|
||||
}
|
||||
else:
|
||||
raise Exception(f"OpenAI API error: {response.status_code} - {response.text}")
|
||||
|
||||
except ImportError:
|
||||
raise Exception("httpx not installed")
|
||||
|
||||
|
||||
def speak_text(text: str, voice: str = "alloy", format: str = "mp3") -> Optional[bytes]:
|
||||
"""
|
||||
Convert text to speech using OpenAI TTS, Coqui, or browser fallback.
|
||||
|
||||
Args:
|
||||
text: Text to convert to speech
|
||||
voice: Voice selection (depends on TTS engine)
|
||||
format: Audio format (mp3, wav, opus)
|
||||
|
||||
Returns:
|
||||
Audio bytes or None if TTS not available
|
||||
"""
|
||||
# Try OpenAI TTS if configured
|
||||
if os.getenv("OPENAI_API_KEY"):
|
||||
try:
|
||||
return _tts_with_openai(text, voice, format)
|
||||
except Exception as e:
|
||||
print(f"OpenAI TTS failed: {e}")
|
||||
|
||||
# Try local Coqui TTS
|
||||
try:
|
||||
return _tts_with_coqui(text)
|
||||
except Exception as e:
|
||||
print(f"Coqui TTS failed: {e}")
|
||||
|
||||
# Return None to signal browser should handle TTS
|
||||
return None
|
||||
|
||||
|
||||
def _tts_with_openai(text: str, voice: str, format: str) -> bytes:
|
||||
"""
|
||||
Text-to-speech using OpenAI TTS API.
|
||||
|
||||
Args:
|
||||
text: Text to speak
|
||||
voice: Voice name (alloy, echo, fable, onyx, nova, shimmer)
|
||||
format: Audio format
|
||||
|
||||
Returns:
|
||||
Audio bytes
|
||||
"""
|
||||
try:
|
||||
import httpx
|
||||
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
raise Exception("OPENAI_API_KEY not configured")
|
||||
|
||||
# Valid voices for OpenAI TTS
|
||||
valid_voices = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
|
||||
if voice not in valid_voices:
|
||||
voice = "alloy"
|
||||
|
||||
# Valid formats
|
||||
valid_formats = ["mp3", "opus", "aac", "flac"]
|
||||
if format not in valid_formats:
|
||||
format = "mp3"
|
||||
|
||||
with httpx.Client() as client:
|
||||
response = client.post(
|
||||
'https://api.openai.com/v1/audio/speech',
|
||||
headers={
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
json={
|
||||
'model': 'tts-1', # or 'tts-1-hd' for higher quality
|
||||
'input': text[:4096], # Max 4096 characters
|
||||
'voice': voice,
|
||||
'response_format': format
|
||||
},
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.content
|
||||
else:
|
||||
raise Exception(f"OpenAI TTS error: {response.status_code} - {response.text}")
|
||||
|
||||
except ImportError:
|
||||
raise Exception("httpx not installed")
|
||||
|
||||
|
||||
def _tts_with_coqui(text: str) -> bytes:
|
||||
"""
|
||||
Text-to-speech using Coqui TTS (local).
|
||||
|
||||
Args:
|
||||
text: Text to speak
|
||||
|
||||
Returns:
|
||||
Audio bytes (WAV format)
|
||||
"""
|
||||
try:
|
||||
from TTS.api import TTS
|
||||
import numpy as np
|
||||
import io
|
||||
import wave
|
||||
|
||||
# Initialize TTS with a fast model
|
||||
tts = TTS(model_name="tts_models/en/ljspeech/tacotron2-DDC", progress_bar=False)
|
||||
|
||||
# Generate speech
|
||||
wav = tts.tts(text)
|
||||
|
||||
# Convert to WAV bytes
|
||||
wav_io = io.BytesIO()
|
||||
with wave.open(wav_io, 'wb') as wav_file:
|
||||
wav_file.setnchannels(1)
|
||||
wav_file.setsampwidth(2)
|
||||
wav_file.setframerate(22050)
|
||||
wav_file.writeframes(np.array(wav * 32767, dtype=np.int16).tobytes())
|
||||
|
||||
return wav_io.getvalue()
|
||||
|
||||
except ImportError:
|
||||
raise Exception("Coqui TTS not installed. Install with: pip install TTS")
|
||||
|
||||
|
||||
def parse_voice_command(text: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse voice command text to extract intent and parameters.
|
||||
|
||||
Args:
|
||||
text: Transcribed voice command text
|
||||
|
||||
Returns:
|
||||
Dictionary with command intent and parameters
|
||||
{
|
||||
"intent": "list_agents" | "summarize" | "deploy_agent" | "run_scan" | "unknown",
|
||||
"parameters": {...},
|
||||
"confidence": 0.0-1.0
|
||||
}
|
||||
"""
|
||||
text_lower = text.lower().strip()
|
||||
|
||||
# Command patterns
|
||||
patterns = [
|
||||
# List commands
|
||||
(r'\b(list|show|display)\s+(agents|scans|findings|results)\b', 'list', lambda m: {'target': m.group(2)}),
|
||||
|
||||
# Summarize commands
|
||||
(r'\b(summarize|summary of|sum up)\s+(findings|results|scan)\b', 'summarize', lambda m: {'target': m.group(2)}),
|
||||
|
||||
# Deploy/start commands
|
||||
(r'\b(deploy|start|launch|run)\s+agent\s+(?:on\s+)?(.+)', 'deploy_agent', lambda m: {'target': m.group(2).strip()}),
|
||||
|
||||
# Scan commands
|
||||
(r'\b(scan|nmap|enumerate)\s+(.+?)(?:\s+(?:using|with)\s+(\w+))?$', 'run_scan',
|
||||
lambda m: {'target': m.group(2).strip(), 'tool': m.group(3) if m.group(3) else 'nmap'}),
|
||||
|
||||
# Status commands
|
||||
(r'\b(status|what\'?s\s+(?:the\s+)?status)\b', 'get_status', lambda m: {}),
|
||||
|
||||
# Help commands
|
||||
(r'\b(help|how\s+do\s+i|assist)\b', 'help', lambda m: {'query': text}),
|
||||
|
||||
# Clear/stop commands
|
||||
(r'\b(stop|cancel|clear)\s+(scan|all|everything)\b', 'stop', lambda m: {'target': m.group(2)}),
|
||||
|
||||
# Navigate commands
|
||||
(r'\b(go\s+to|open|navigate\s+to)\s+(.+)', 'navigate', lambda m: {'destination': m.group(2).strip()}),
|
||||
]
|
||||
|
||||
# Try to match patterns
|
||||
for pattern, intent, param_func in patterns:
|
||||
match = re.search(pattern, text_lower)
|
||||
if match:
|
||||
try:
|
||||
parameters = param_func(match)
|
||||
return {
|
||||
"intent": intent,
|
||||
"parameters": parameters,
|
||||
"confidence": 0.85,
|
||||
"raw_text": text
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error parsing command parameters: {e}")
|
||||
|
||||
# No pattern matched
|
||||
return {
|
||||
"intent": "unknown",
|
||||
"parameters": {},
|
||||
"confidence": 0.0,
|
||||
"raw_text": text
|
||||
}
|
||||
|
||||
|
||||
def route_command(command_result: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Route a parsed voice command to the appropriate action.
|
||||
|
||||
Args:
|
||||
command_result: Result from parse_voice_command()
|
||||
|
||||
Returns:
|
||||
Dictionary with routing information
|
||||
{
|
||||
"action": "api_call" | "navigate" | "notify" | "error",
|
||||
"endpoint": "/api/...",
|
||||
"method": "GET" | "POST",
|
||||
"data": {...},
|
||||
"message": "Human-readable action description"
|
||||
}
|
||||
"""
|
||||
intent = command_result.get("intent")
|
||||
params = command_result.get("parameters", {})
|
||||
|
||||
if intent == "list":
|
||||
target = params.get("target", "")
|
||||
endpoint_map = {
|
||||
"agents": "/api/agents",
|
||||
"scans": "/api/scans",
|
||||
"findings": "/api/findings",
|
||||
"results": "/api/results"
|
||||
}
|
||||
endpoint = endpoint_map.get(target, "/api/scans")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": endpoint,
|
||||
"method": "GET",
|
||||
"data": {},
|
||||
"message": f"Fetching {target}..."
|
||||
}
|
||||
|
||||
elif intent == "summarize":
|
||||
target = params.get("target", "findings")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/summarize",
|
||||
"method": "POST",
|
||||
"data": {"target": target},
|
||||
"message": f"Summarizing {target}..."
|
||||
}
|
||||
|
||||
elif intent == "deploy_agent":
|
||||
target = params.get("target", "")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/agents/deploy",
|
||||
"method": "POST",
|
||||
"data": {"target": target},
|
||||
"message": f"Deploying agent to {target}..."
|
||||
}
|
||||
|
||||
elif intent == "run_scan":
|
||||
target = params.get("target", "")
|
||||
tool = params.get("tool", "nmap")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/scan",
|
||||
"method": "POST",
|
||||
"data": {
|
||||
"tool": tool,
|
||||
"target": target,
|
||||
"scan_type": "quick"
|
||||
},
|
||||
"message": f"Starting {tool} scan of {target}..."
|
||||
}
|
||||
|
||||
elif intent == "get_status":
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/status",
|
||||
"method": "GET",
|
||||
"data": {},
|
||||
"message": "Checking system status..."
|
||||
}
|
||||
|
||||
elif intent == "help":
|
||||
query = params.get("query", "")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/llm/chat",
|
||||
"method": "POST",
|
||||
"data": {"message": query, "context": "help_request"},
|
||||
"message": "Getting help..."
|
||||
}
|
||||
|
||||
elif intent == "stop":
|
||||
target = params.get("target", "all")
|
||||
return {
|
||||
"action": "api_call",
|
||||
"endpoint": "/api/scans/clear" if target in ["all", "everything"] else "/api/scan/stop",
|
||||
"method": "DELETE",
|
||||
"data": {},
|
||||
"message": f"Stopping {target}..."
|
||||
}
|
||||
|
||||
elif intent == "navigate":
|
||||
destination = params.get("destination", "")
|
||||
# Map common destinations
|
||||
destination_map = {
|
||||
"dashboard": "/",
|
||||
"home": "/",
|
||||
"terminal": "/terminal",
|
||||
"scans": "/scans",
|
||||
"settings": "/settings"
|
||||
}
|
||||
path = destination_map.get(destination, f"/{destination}")
|
||||
return {
|
||||
"action": "navigate",
|
||||
"endpoint": path,
|
||||
"method": "GET",
|
||||
"data": {},
|
||||
"message": f"Navigating to {destination}..."
|
||||
}
|
||||
|
||||
else:
|
||||
# Unknown intent - return error
|
||||
return {
|
||||
"action": "error",
|
||||
"endpoint": "",
|
||||
"method": "",
|
||||
"data": {},
|
||||
"message": "I didn't understand that command. Try 'help' for available commands.",
|
||||
"error": "unknown_intent"
|
||||
}
|
||||
|
||||
|
||||
def get_voice_command_help() -> Dict[str, list]:
|
||||
"""
|
||||
Get list of available voice commands.
|
||||
|
||||
Returns:
|
||||
Dictionary categorized by command type
|
||||
"""
|
||||
return {
|
||||
"navigation": [
|
||||
"Go to dashboard",
|
||||
"Open terminal",
|
||||
"Navigate to scans"
|
||||
],
|
||||
"scanning": [
|
||||
"Scan 192.168.1.1",
|
||||
"Run nmap scan on example.com",
|
||||
"Start scan of 10.0.0.0/24"
|
||||
],
|
||||
"information": [
|
||||
"List scans",
|
||||
"Show agents",
|
||||
"Display findings",
|
||||
"What's the status"
|
||||
],
|
||||
"actions": [
|
||||
"Deploy agent on target.com",
|
||||
"Stop all scans",
|
||||
"Clear everything",
|
||||
"Summarize findings"
|
||||
],
|
||||
"help": [
|
||||
"Help me with nmap",
|
||||
"How do I scan a network",
|
||||
"Assist with reconnaissance"
|
||||
]
|
||||
}
|
||||
@@ -2,3 +2,4 @@ fastapi==0.115.5
|
||||
uvicorn[standard]==0.32.1
|
||||
httpx==0.28.1
|
||||
pydantic==2.10.2
|
||||
python-multipart==0.0.9
|
||||
|
||||
@@ -17,23 +17,55 @@ from datetime import datetime
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
# Allowed command prefixes (security whitelist)
|
||||
# Expanded to support all Kali tools
|
||||
ALLOWED_COMMANDS = {
|
||||
# Reconnaissance
|
||||
"nmap", "masscan", "amass", "theharvester", "whatweb", "dnsrecon", "fierce",
|
||||
"dig", "nslookup", "host", "whois",
|
||||
"dig", "nslookup", "host", "whois", "recon-ng", "maltego", "dmitry", "dnsenum",
|
||||
"enum4linux", "nbtscan", "onesixtyone", "smbclient", "snmp-check", "wafw00f",
|
||||
# Web testing
|
||||
"nikto", "gobuster", "dirb", "sqlmap", "wpscan", "curl", "wget",
|
||||
"nikto", "gobuster", "dirb", "sqlmap", "wpscan", "curl", "wget", "burpsuite",
|
||||
"zaproxy", "zap-cli", "wfuzz", "ffuf", "dirbuster", "cadaver", "davtest",
|
||||
"skipfish", "uniscan", "whatweb", "wapiti", "commix", "joomscan", "droopescan",
|
||||
# Wireless
|
||||
"aircrack-ng", "airodump-ng", "aireplay-ng", "airmon-ng", "airbase-ng",
|
||||
"wifite", "reaver", "bully", "kismet", "fern-wifi-cracker", "wash", "cowpatty",
|
||||
"mdk3", "mdk4", "pixiewps", "wifiphisher", "eaphammer", "hostapd-wpe",
|
||||
# Password attacks
|
||||
"hydra", "medusa", "john", "hashcat", "ncrack", "patator", "ophcrack",
|
||||
"crunch", "cewl", "rsmangler", "hashid", "hash-identifier",
|
||||
# Network utilities
|
||||
"ping", "traceroute", "netcat", "nc", "tcpdump",
|
||||
# Exploitation research
|
||||
"searchsploit", "msfconsole", "msfvenom",
|
||||
# Brute force
|
||||
"hydra", "medusa",
|
||||
"ping", "traceroute", "netcat", "nc", "tcpdump", "wireshark", "tshark",
|
||||
"ettercap", "bettercap", "responder", "arpspoof", "dnsspoof", "macchanger",
|
||||
"hping3", "arping", "fping", "masscan-web", "unicornscan",
|
||||
# Exploitation
|
||||
"searchsploit", "msfconsole", "msfvenom", "exploit", "armitage",
|
||||
"beef-xss", "set", "setoolkit", "backdoor-factory", "shellnoob",
|
||||
"commix", "routersploit", "linux-exploit-suggester",
|
||||
# Post-exploitation
|
||||
"mimikatz", "powersploit", "empire", "covenant", "crackmapexec", "cme",
|
||||
"impacket-smbserver", "impacket-psexec", "evil-winrm", "bloodhound",
|
||||
"sharphound", "powershell", "pwsh",
|
||||
# Forensics
|
||||
"autopsy", "volatility", "sleuthkit", "foremost", "binwalk", "bulk-extractor",
|
||||
"scalpel", "dc3dd", "guymager", "chkrootkit", "rkhunter",
|
||||
# Reverse engineering
|
||||
"ghidra", "radare2", "r2", "gdb", "objdump", "strings", "ltrace", "strace",
|
||||
"hexdump", "xxd", "file", "readelf", "checksec", "pwntools",
|
||||
# Sniffing
|
||||
"dsniff", "tcpflow", "tcpreplay", "tcpick", "ngrep", "p0f", "ssldump",
|
||||
# System info
|
||||
"ls", "cat", "head", "tail", "grep", "find", "pwd", "whoami", "id",
|
||||
"uname", "hostname", "ip", "ifconfig", "netstat", "ss",
|
||||
"uname", "hostname", "ip", "ifconfig", "netstat", "ss", "route",
|
||||
# Analysis tools
|
||||
"exiftool", "pdfid", "pdf-parser", "peepdf", "oletools", "olevba",
|
||||
# VPN/Tunneling
|
||||
"openvpn", "ssh", "sshuttle", "proxychains", "tor", "socat",
|
||||
# Misc security tools
|
||||
"openssl", "gpg", "steghide", "outguess", "covert", "stegosuite",
|
||||
"yersinia", "responder", "chisel", "ligolo", "sliver",
|
||||
# Python scripts
|
||||
"python", "python3",
|
||||
"python", "python3", "python2",
|
||||
}
|
||||
|
||||
# Blocked patterns (dangerous commands)
|
||||
@@ -466,6 +498,88 @@ async def list_installed_tools():
|
||||
return {"installed_tools": installed}
|
||||
|
||||
|
||||
@app.get("/captured_commands")
|
||||
async def get_captured_commands(limit: int = 50, since: Optional[str] = None):
|
||||
"""
|
||||
Get commands that were captured from interactive shell sessions in the Kali container.
|
||||
These are commands run directly by users via docker exec or SSH.
|
||||
"""
|
||||
global kali_container
|
||||
|
||||
if not kali_container:
|
||||
raise HTTPException(status_code=503, detail="Kali container not available")
|
||||
|
||||
try:
|
||||
kali_container.reload()
|
||||
|
||||
# Read command history from the shared volume
|
||||
cmd = ["bash", "-c", "cd /workspace/.command_history && ls -t *.json 2>/dev/null | head -n {}".format(limit)]
|
||||
exit_code, output = kali_container.exec_run(cmd=cmd, demux=True)
|
||||
|
||||
if exit_code != 0 or not output[0]:
|
||||
return {"commands": [], "count": 0}
|
||||
|
||||
# Get list of log files
|
||||
log_files = output[0].decode('utf-8', errors='replace').strip().split('\n')
|
||||
log_files = [f for f in log_files if f.strip()]
|
||||
|
||||
commands = []
|
||||
for log_file in log_files:
|
||||
try:
|
||||
# Read each JSON log file
|
||||
read_cmd = ["cat", f"/workspace/.command_history/{log_file}"]
|
||||
exit_code, output = kali_container.exec_run(cmd=read_cmd, demux=True)
|
||||
|
||||
if exit_code == 0 and output[0]:
|
||||
cmd_data = json.loads(output[0].decode('utf-8', errors='replace'))
|
||||
|
||||
# Filter by timestamp if requested
|
||||
if since:
|
||||
cmd_timestamp = cmd_data.get("timestamp", "")
|
||||
if cmd_timestamp < since:
|
||||
continue
|
||||
|
||||
commands.append(cmd_data)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return {
|
||||
"commands": commands,
|
||||
"count": len(commands),
|
||||
"source": "interactive_shell_capture"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error reading captured commands: {str(e)}")
|
||||
|
||||
|
||||
@app.delete("/captured_commands/clear")
|
||||
async def clear_captured_commands():
|
||||
"""Clear all captured command history."""
|
||||
global kali_container
|
||||
|
||||
if not kali_container:
|
||||
raise HTTPException(status_code=503, detail="Kali container not available")
|
||||
|
||||
try:
|
||||
kali_container.reload()
|
||||
|
||||
# Clear the command history directory
|
||||
cmd = ["bash", "-c", "rm -f /workspace/.command_history/*.json"]
|
||||
exit_code, _ = kali_container.exec_run(cmd=cmd)
|
||||
|
||||
if exit_code == 0:
|
||||
return {"status": "cleared", "message": "All captured command history cleared"}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Failed to clear history")
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/allowed-commands")
|
||||
async def get_allowed_commands():
|
||||
"""Get list of allowed commands for security validation."""
|
||||
|
||||
@@ -3,45 +3,38 @@ FROM kalilinux/kali-rolling
|
||||
# Avoid prompts during package installation
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Configure apt to use direct Kali mirrors (avoid CDN mirrors that get blocked by content filters)
|
||||
# The mirror.us.cdn-perfprod.com CDN is being blocked by SafeBrowse content filter
|
||||
RUN echo 'deb http://kali.download/kali kali-rolling main non-free non-free-firmware contrib' > /etc/apt/sources.list && \
|
||||
echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries
|
||||
|
||||
# Update and install essential security tools
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
# Core utilities
|
||||
nmap \
|
||||
nikto \
|
||||
sqlmap \
|
||||
gobuster \
|
||||
dirb \
|
||||
wfuzz \
|
||||
hydra \
|
||||
john \
|
||||
hashcat \
|
||||
whois \
|
||||
dnsutils \
|
||||
net-tools \
|
||||
iputils-ping \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
vim \
|
||||
net-tools \
|
||||
iputils-ping \
|
||||
dnsutils \
|
||||
# Reconnaissance tools
|
||||
nmap \
|
||||
masscan \
|
||||
amass \
|
||||
theharvester \
|
||||
whatweb \
|
||||
dnsrecon \
|
||||
fierce \
|
||||
# Web testing tools
|
||||
nikto \
|
||||
gobuster \
|
||||
dirb \
|
||||
sqlmap \
|
||||
# Network tools
|
||||
netcat-openbsd \
|
||||
tcpdump \
|
||||
wireshark-common \
|
||||
hydra \
|
||||
# Exploitation
|
||||
metasploit-framework \
|
||||
exploitdb \
|
||||
# Scripting
|
||||
jq \
|
||||
uuid-runtime \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install additional Python tools
|
||||
# Install additional Python tools for command logging and scripting
|
||||
RUN pip3 install --break-system-packages \
|
||||
requests \
|
||||
beautifulsoup4 \
|
||||
@@ -51,8 +44,17 @@ RUN pip3 install --break-system-packages \
|
||||
# Create workspace directory
|
||||
WORKDIR /workspace
|
||||
|
||||
# Copy entrypoint script
|
||||
# Copy scripts and convert Windows line endings to Unix
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
COPY command_logger.sh /usr/local/bin/command_logger.sh
|
||||
COPY capture_wrapper.sh /usr/local/bin/capture
|
||||
COPY nmap_wrapper.sh /usr/local/bin/nmap
|
||||
|
||||
# Convert any Windows line endings (CRLF) to Unix (LF)
|
||||
RUN sed -i 's/\r$//' /entrypoint.sh /usr/local/bin/command_logger.sh /usr/local/bin/capture /usr/local/bin/nmap
|
||||
RUN chmod +x /entrypoint.sh /usr/local/bin/command_logger.sh /usr/local/bin/capture /usr/local/bin/nmap
|
||||
|
||||
# Create command history directory
|
||||
RUN mkdir -p /workspace/.command_history
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
101
services/kali/capture_wrapper.sh
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/bin/bash
|
||||
# Output Capture Wrapper for Security Tools
|
||||
# Wraps command execution to capture stdout/stderr and save results
|
||||
# Automatically sends nmap results to dashboard network map
|
||||
|
||||
COMMAND_LOG_DIR="${COMMAND_LOG_DIR:-/workspace/.command_history}"
|
||||
DASHBOARD_URL="${DASHBOARD_URL:-http://strikepackage-dashboard:8080}"
|
||||
mkdir -p "$COMMAND_LOG_DIR"
|
||||
|
||||
# Get command from arguments
|
||||
cmd_string="$@"
|
||||
[[ -z "$cmd_string" ]] && exit 1
|
||||
|
||||
# Generate unique ID
|
||||
cmd_id=$(uuidgen 2>/dev/null || echo "$(date +%s)-$$")
|
||||
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
output_file="$COMMAND_LOG_DIR/${cmd_id}.json"
|
||||
stdout_file="$COMMAND_LOG_DIR/${cmd_id}.stdout"
|
||||
stderr_file="$COMMAND_LOG_DIR/${cmd_id}.stderr"
|
||||
|
||||
# Create initial log entry
|
||||
cat > "$output_file" << EOF
|
||||
{
|
||||
"command_id": "$cmd_id",
|
||||
"command": $(echo "$cmd_string" | jq -Rs .),
|
||||
"timestamp": "$timestamp",
|
||||
"user": "$(whoami)",
|
||||
"working_dir": "$(pwd)",
|
||||
"source": "capture_wrapper",
|
||||
"status": "running"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Execute command and capture output
|
||||
start_time=$(date +%s)
|
||||
set +e
|
||||
eval "$cmd_string" > "$stdout_file" 2> "$stderr_file"
|
||||
exit_code=$?
|
||||
set -e
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
completed_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# Read captured output
|
||||
stdout_content=$(cat "$stdout_file" 2>/dev/null || echo "")
|
||||
stderr_content=$(cat "$stderr_file" 2>/dev/null || echo "")
|
||||
|
||||
# Update log entry with results
|
||||
cat > "$output_file" << EOF
|
||||
{
|
||||
"command_id": "$cmd_id",
|
||||
"command": $(echo "$cmd_string" | jq -Rs .),
|
||||
"timestamp": "$timestamp",
|
||||
"completed_at": "$completed_at",
|
||||
"user": "$(whoami)",
|
||||
"working_dir": "$(pwd)",
|
||||
"source": "capture_wrapper",
|
||||
"status": "$([ $exit_code -eq 0 ] && echo 'completed' || echo 'failed')",
|
||||
"exit_code": $exit_code,
|
||||
"duration": $duration,
|
||||
"stdout": $(echo "$stdout_content" | jq -Rs .),
|
||||
"stderr": $(echo "$stderr_content" | jq -Rs .)
|
||||
}
|
||||
EOF
|
||||
|
||||
# Output results to terminal first
|
||||
echo "$stdout_content"
|
||||
[ -n "$stderr_content" ] && echo "$stderr_content" >&2
|
||||
|
||||
# Clean up temp files
|
||||
rm -f "$stdout_file" "$stderr_file"
|
||||
|
||||
# If this was an nmap command, send results to dashboard network map
|
||||
if [[ "$cmd_string" == nmap* ]] && [ $exit_code -eq 0 ]; then
|
||||
echo "" >&2
|
||||
echo "[StrikePackageGPT] Detected nmap scan, sending to Network Map..." >&2
|
||||
|
||||
# Send nmap output to dashboard for parsing
|
||||
nmap_json=$(jq -n --arg output "$stdout_content" --arg source "terminal" \
|
||||
'{output: $output, source: $source}')
|
||||
|
||||
response=$(curl -s -X POST "${DASHBOARD_URL}/api/network/nmap-results" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$nmap_json" 2>/dev/null || echo '{"error":"failed to connect"}')
|
||||
|
||||
# Parse response
|
||||
added=$(echo "$response" | jq -r '.added // 0' 2>/dev/null)
|
||||
updated=$(echo "$response" | jq -r '.updated // 0' 2>/dev/null)
|
||||
total=$(echo "$response" | jq -r '.total // 0' 2>/dev/null)
|
||||
|
||||
if [ "$added" != "null" ] && [ "$added" != "0" -o "$updated" != "0" ]; then
|
||||
echo "[StrikePackageGPT] Network Map updated: $added added, $updated updated (total: $total hosts)" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "" >&2
|
||||
echo "[StrikePackageGPT] Command captured: $cmd_id" >&2
|
||||
echo "[StrikePackageGPT] Exit code: $exit_code | Duration: ${duration}s" >&2
|
||||
echo "[StrikePackageGPT] Results available in dashboard" >&2
|
||||
|
||||
exit $exit_code
|
||||
53
services/kali/command_logger.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# Command Logger for StrikePackageGPT
|
||||
# Logs all commands executed in interactive shell sessions
|
||||
# Results are captured and made available to the API
|
||||
|
||||
COMMAND_LOG_DIR="${COMMAND_LOG_DIR:-/workspace/.command_history}"
|
||||
mkdir -p "$COMMAND_LOG_DIR"
|
||||
|
||||
# Function to log command execution
|
||||
log_command() {
|
||||
local cmd="$1"
|
||||
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
local cmd_id=$(uuidgen 2>/dev/null || echo "$(date +%s)-$$")
|
||||
local output_file="$COMMAND_LOG_DIR/${cmd_id}.json"
|
||||
|
||||
# Skip logging for cd, ls, echo, and other basic commands
|
||||
local first_word=$(echo "$cmd" | awk '{print $1}')
|
||||
case "$first_word" in
|
||||
cd|ls|pwd|echo|exit|clear|history|source|alias|\
|
||||
export|unset|env|printenv|which|type|whereis)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Skip empty commands
|
||||
[[ -z "$cmd" ]] && return 0
|
||||
|
||||
# Create log entry with metadata
|
||||
cat > "$output_file" << EOF
|
||||
{
|
||||
"command_id": "$cmd_id",
|
||||
"command": $(echo "$cmd" | jq -Rs .),
|
||||
"timestamp": "$timestamp",
|
||||
"user": "$(whoami)",
|
||||
"working_dir": "$(pwd)",
|
||||
"source": "interactive_shell",
|
||||
"status": "pending"
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "[StrikePackageGPT] Command logged: $cmd_id" >&2
|
||||
echo "[StrikePackageGPT] Results will be visible in dashboard" >&2
|
||||
}
|
||||
|
||||
# PROMPT_COMMAND hook to log each command after execution
|
||||
export PROMPT_COMMAND='history -a; if [ -n "$LAST_CMD" ]; then log_command "$LAST_CMD"; fi; LAST_CMD=$(history 1 | sed "s/^[ ]*[0-9]*[ ]*//"); '
|
||||
|
||||
# Also trap DEBUG for more comprehensive logging
|
||||
trap 'LAST_EXEC_CMD="$BASH_COMMAND"' DEBUG
|
||||
|
||||
echo "[StrikePackageGPT] Command logging enabled"
|
||||
echo "[StrikePackageGPT] All security tool commands will be captured and visible in the dashboard"
|
||||
echo ""
|
||||
@@ -1,18 +1,95 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Enable command logging by default for all bash sessions
|
||||
echo 'source /usr/local/bin/command_logger.sh' >> /root/.bashrc
|
||||
echo 'export COMMAND_LOG_DIR=/workspace/.command_history' >> /root/.bashrc
|
||||
echo 'export DASHBOARD_URL=http://strikepackage-dashboard:8080' >> /root/.bashrc
|
||||
|
||||
# Create convenience aliases for captured execution
|
||||
cat >> /root/.bashrc << 'ALIASES'
|
||||
# Convenience alias to run commands with automatic capture
|
||||
alias run='capture'
|
||||
|
||||
# Wrap nmap to automatically send results to network map
|
||||
nmap_wrapper() {
|
||||
local output
|
||||
local exit_code
|
||||
|
||||
# Run nmap and capture output
|
||||
output=$(/usr/bin/nmap "$@" 2>&1)
|
||||
exit_code=$?
|
||||
|
||||
# Display output
|
||||
echo "$output"
|
||||
|
||||
# If successful, send to dashboard network map
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo "" >&2
|
||||
echo "[StrikePackageGPT] Sending nmap results to Network Map..." >&2
|
||||
|
||||
# Send to dashboard
|
||||
response=$(curl -s -X POST "${DASHBOARD_URL:-http://strikepackage-dashboard:8080}/api/network/nmap-results" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg output "$output" --arg source "terminal" '{output: $output, source: $source}')" 2>/dev/null)
|
||||
|
||||
added=$(echo "$response" | jq -r '.added // 0' 2>/dev/null)
|
||||
updated=$(echo "$response" | jq -r '.updated // 0' 2>/dev/null)
|
||||
total=$(echo "$response" | jq -r '.total // 0' 2>/dev/null)
|
||||
|
||||
if [ "$added" != "null" ] 2>/dev/null; then
|
||||
echo "[StrikePackageGPT] Network Map: $added added, $updated updated (total: $total hosts)" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
return $exit_code
|
||||
}
|
||||
alias nmap='nmap_wrapper'
|
||||
|
||||
# Helper function to show recent commands
|
||||
recent_commands() {
|
||||
echo "Recent commands logged:"
|
||||
ls -lt /workspace/.command_history/*.json 2>/dev/null | head -10 | while read line; do
|
||||
file=$(echo "$line" | awk '{print $NF}')
|
||||
[ -f "$file" ] && jq -r '"\(.timestamp) - \(.command) [\(.status)]"' "$file" 2>/dev/null
|
||||
done
|
||||
}
|
||||
alias recent='recent_commands'
|
||||
|
||||
# Show network map hosts
|
||||
show_hosts() {
|
||||
echo "Network Map Hosts:"
|
||||
curl -s "${DASHBOARD_URL:-http://strikepackage-dashboard:8080}/api/network/hosts" | jq -r '.hosts[] | "\(.ip)\t\(.hostname // "-")\t\(.os // "-")\tPorts: \(.ports | length)"' 2>/dev/null || echo "No hosts found"
|
||||
}
|
||||
alias hosts='show_hosts'
|
||||
|
||||
# Clear network map
|
||||
clear_hosts() {
|
||||
curl -s -X DELETE "${DASHBOARD_URL:-http://strikepackage-dashboard:8080}/api/network/hosts" | jq .
|
||||
echo "Network map cleared"
|
||||
}
|
||||
ALIASES
|
||||
|
||||
echo "=================================================="
|
||||
echo " StrikePackageGPT - Kali Container"
|
||||
echo " Security Tools Ready"
|
||||
echo " Security Tools Ready + Network Map Integration"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
echo "Available tools:"
|
||||
echo " - nmap, masscan (port scanning)"
|
||||
echo " - amass, theharvester (reconnaissance)"
|
||||
echo " - nikto, gobuster (web testing)"
|
||||
echo " - sqlmap (SQL injection)"
|
||||
echo " - hydra (brute force)"
|
||||
echo " - metasploit (exploitation)"
|
||||
echo " - searchsploit (exploit database)"
|
||||
echo " - john, hashcat (password cracking)"
|
||||
echo ""
|
||||
echo "🗺️ NETWORK MAP INTEGRATION ENABLED 🗺️"
|
||||
echo ""
|
||||
echo "nmap scans automatically appear in the Dashboard Network Map!"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " • nmap -sV 192.168.1.1 - Scan and auto-add to map"
|
||||
echo " • hosts - Show network map hosts"
|
||||
echo " • clear_hosts - Clear network map"
|
||||
echo " • recent - Show recent commands"
|
||||
echo ""
|
||||
echo "Container is ready for security testing."
|
||||
echo ""
|
||||
|
||||
50
services/kali/nmap_wrapper.sh
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# StrikePackageGPT nmap wrapper - sends scan results to Network Map and Scan History automatically
|
||||
|
||||
DASHBOARD_URL="${DASHBOARD_URL:-http://strikepackage-dashboard:8080}"
|
||||
REAL_NMAP="/usr/bin/nmap"
|
||||
|
||||
# Capture the full command for logging
|
||||
full_command="nmap $*"
|
||||
|
||||
# Determine target (last non-flag argument)
|
||||
target="unknown"
|
||||
for arg in "$@"; do
|
||||
if [[ ! "$arg" =~ ^- ]]; then
|
||||
target="$arg"
|
||||
fi
|
||||
done
|
||||
|
||||
# Create temp file for output
|
||||
tmpfile=$(mktemp)
|
||||
trap "rm -f $tmpfile" EXIT
|
||||
|
||||
# Run the actual nmap and capture output
|
||||
"$REAL_NMAP" "$@" 2>&1 | tee "$tmpfile"
|
||||
exit_code=${PIPESTATUS[0]}
|
||||
|
||||
# If successful, send to dashboard
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo "" >&2
|
||||
echo "[StrikePackageGPT] Sending results to Dashboard..." >&2
|
||||
|
||||
# Use jq with file input to avoid argument length limits
|
||||
# Send to network map
|
||||
jq -Rs --arg source "terminal" '{output: ., source: $source}' "$tmpfile" | \
|
||||
curl -s -X POST "${DASHBOARD_URL}/api/network/nmap-results" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- >/dev/null 2>&1
|
||||
|
||||
# Send to scan history
|
||||
response=$(jq -Rs --arg tool "nmap" --arg target "$target" --arg command "$full_command" \
|
||||
'{tool: $tool, target: $target, command: $command, output: ., source: "terminal"}' "$tmpfile" | \
|
||||
curl -s -X POST "${DASHBOARD_URL}/api/scans/terminal" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- 2>/dev/null)
|
||||
|
||||
if [ -n "$response" ]; then
|
||||
echo "[StrikePackageGPT] ✓ Results saved to Network Map and Scan History" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
exit $exit_code
|
||||
121
test_services.sh
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
# StrikePackageGPT Service Test Script
|
||||
|
||||
echo "=========================================="
|
||||
echo " StrikePackageGPT V2.1 Test Suite"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Test 1: Health Endpoints
|
||||
echo "=== TEST 1: Health Endpoints ==="
|
||||
echo "LLM Router:"
|
||||
curl -s http://strikepackage-llm-router:8000/health | jq .
|
||||
echo ""
|
||||
echo "HackGPT API:"
|
||||
curl -s http://strikepackage-hackgpt-api:8001/health | jq .
|
||||
echo ""
|
||||
echo "Kali Executor:"
|
||||
curl -s http://strikepackage-kali-executor:8002/health | jq .
|
||||
echo ""
|
||||
|
||||
# Test 2: LLM Router
|
||||
echo "=== TEST 2: LLM Router ==="
|
||||
echo "Providers:"
|
||||
curl -s http://strikepackage-llm-router:8000/providers | jq -r 'keys[]'
|
||||
echo ""
|
||||
|
||||
echo "Chat Test (llama3.1:latest):"
|
||||
cat > /tmp/chat.json << 'EOFCHAT'
|
||||
{"provider":"ollama","model":"llama3.1:latest","messages":[{"role":"user","content":"Say hello in exactly 3 words"}]}
|
||||
EOFCHAT
|
||||
RESPONSE=$(curl -s -X POST http://strikepackage-llm-router:8000/chat -H "Content-Type: application/json" -d @/tmp/chat.json)
|
||||
echo "$RESPONSE" | jq -r '.response // .content // .message // .' | head -c 200
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
# Test 3: HackGPT API
|
||||
echo "=== TEST 3: HackGPT API ==="
|
||||
echo "Tools available:"
|
||||
curl -s http://strikepackage-hackgpt-api:8001/tools | jq -r '.[].name' 2>/dev/null || curl -s http://strikepackage-hackgpt-api:8001/tools | head -c 300
|
||||
echo ""
|
||||
|
||||
echo "Scans list:"
|
||||
curl -s http://strikepackage-hackgpt-api:8001/scans | jq . 2>/dev/null || echo "[]"
|
||||
echo ""
|
||||
|
||||
# Test 4: Kali Executor
|
||||
echo "=== TEST 4: Kali Executor ==="
|
||||
echo "Tools:"
|
||||
curl -s http://strikepackage-kali-executor:8002/tools | jq -r '.tools[:5][]' 2>/dev/null || curl -s http://strikepackage-kali-executor:8002/tools | head -c 200
|
||||
echo ""
|
||||
|
||||
echo "Execute nmap version:"
|
||||
cat > /tmp/exec.json << 'EOFEXEC'
|
||||
{"command":"nmap --version","timeout":30}
|
||||
EOFEXEC
|
||||
curl -s -X POST http://strikepackage-kali-executor:8002/execute -H "Content-Type: application/json" -d @/tmp/exec.json | jq -r '.output // .stdout // .' | head -5
|
||||
echo ""
|
||||
|
||||
echo "Jobs:"
|
||||
curl -s http://strikepackage-kali-executor:8002/jobs | jq -r 'length' 2>/dev/null || echo "0"
|
||||
echo " jobs recorded"
|
||||
echo ""
|
||||
|
||||
# Test 5: Security Scans from Kali
|
||||
echo "=== TEST 5: Security Tools in Kali ==="
|
||||
echo "nmap scan of llm-router:"
|
||||
nmap -sT -p 8000 strikepackage-llm-router 2>&1 | grep -E "PORT|open|closed"
|
||||
echo ""
|
||||
|
||||
echo "nikto quick test:"
|
||||
nikto -h http://strikepackage-dashboard:8080 -maxtime 10s 2>&1 | tail -5
|
||||
echo ""
|
||||
|
||||
echo "gobuster test:"
|
||||
gobuster dir -u http://strikepackage-hackgpt-api:8001 -w /usr/share/dirb/wordlists/common.txt -t 5 --timeout 5s 2>&1 | grep -E "Status|Found|Finished" | head -10
|
||||
echo ""
|
||||
|
||||
# Test 6: Dashboard APIs
|
||||
echo "=== TEST 6: Dashboard APIs ==="
|
||||
echo "Status:"
|
||||
curl -s http://strikepackage-dashboard:8080/api/status | jq . 2>/dev/null || curl -s http://strikepackage-dashboard:8080/api/status
|
||||
echo ""
|
||||
|
||||
echo "Processes:"
|
||||
curl -s http://strikepackage-dashboard:8080/api/processes | jq -r 'length' 2>/dev/null
|
||||
echo " processes"
|
||||
echo ""
|
||||
|
||||
# Test 7: End-to-end Chat via Dashboard
|
||||
echo "=== TEST 7: Dashboard Chat ==="
|
||||
cat > /tmp/dashchat.json << 'EOFDASH'
|
||||
{"message":"What is nmap used for? Answer in one sentence."}
|
||||
EOFDASH
|
||||
CHATRESP=$(curl -s -X POST http://strikepackage-dashboard:8080/api/chat -H "Content-Type: application/json" -d @/tmp/dashchat.json)
|
||||
echo "$CHATRESP" | jq -r '.response // .content // .message // .' 2>/dev/null | head -c 300
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
# Test 8: Command Execution via Kali Executor
|
||||
echo "=== TEST 8: Command Execution ==="
|
||||
cat > /tmp/scanexec.json << 'EOFSCAN'
|
||||
{"command":"nmap -sT -p 80,443,8000,8080 strikepackage-dashboard","timeout":60}
|
||||
EOFSCAN
|
||||
echo "Running: nmap scan of dashboard..."
|
||||
EXECRESP=$(curl -s -X POST http://strikepackage-kali-executor:8002/execute -H "Content-Type: application/json" -d @/tmp/scanexec.json)
|
||||
echo "$EXECRESP" | jq -r '.output // .stdout // .' 2>/dev/null | grep -E "PORT|open|closed|filtered" | head -10
|
||||
echo ""
|
||||
|
||||
# Test 9: AI-Assisted Scanning
|
||||
echo "=== TEST 9: AI Scan Request ==="
|
||||
cat > /tmp/aiscan.json << 'EOFAI'
|
||||
{"message":"Scan strikepackage-llm-router for web vulnerabilities"}
|
||||
EOFAI
|
||||
AIRESP=$(curl -s -X POST http://strikepackage-dashboard:8080/api/chat -H "Content-Type: application/json" -d @/tmp/aiscan.json)
|
||||
echo "$AIRESP" | jq -r '.response // .content // .' 2>/dev/null | head -c 400
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo " Tests Complete!"
|
||||
echo "=========================================="
|
||||
134
upload_repo.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
upload_repo.py
|
||||
|
||||
Uploads files from a zip into a GitHub repo branch using the Contents API.
|
||||
|
||||
Environment variables:
|
||||
GITHUB_TOKEN - personal access token (repo scope)
|
||||
REPO - owner/repo (e.g. mblanke/StrikePackageGPT-Lab)
|
||||
BRANCH - target branch name (default: c2-integration)
|
||||
ZIP_FILENAME - name of zip file present in the current directory
|
||||
|
||||
Usage:
|
||||
export GITHUB_TOKEN='ghp_xxx'
|
||||
export REPO='owner/repo'
|
||||
export BRANCH='c2-integration'
|
||||
export ZIP_FILENAME='goose_c2_files.zip'
|
||||
python3 upload_repo.py
|
||||
"""
|
||||
import os, sys, base64, zipfile, requests, time
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
API_BASE = "https://api.github.com"
|
||||
|
||||
def die(msg):
|
||||
print("ERROR:", msg); sys.exit(1)
|
||||
|
||||
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
|
||||
REPO = os.environ.get("REPO")
|
||||
BRANCH = os.environ.get("BRANCH", "c2-integration")
|
||||
ZIP_FILENAME = os.environ.get("ZIP_FILENAME")
|
||||
|
||||
def api_headers():
|
||||
if not GITHUB_TOKEN:
|
||||
die("GITHUB_TOKEN not set")
|
||||
return {"Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json"}
|
||||
|
||||
def get_default_branch():
|
||||
url = f"{API_BASE}/repos/{REPO}"
|
||||
r = requests.get(url, headers=api_headers())
|
||||
if r.status_code != 200:
|
||||
die(f"Failed to get repo info: {r.status_code} {r.text}")
|
||||
return r.json().get("default_branch")
|
||||
|
||||
def get_ref_sha(branch):
|
||||
url = f"{API_BASE}/repos/{REPO}/git/refs/heads/{branch}"
|
||||
r = requests.get(url, headers=api_headers())
|
||||
if r.status_code == 200:
|
||||
return r.json()["object"]["sha"]
|
||||
return None
|
||||
|
||||
def create_branch(new_branch, from_sha):
|
||||
url = f"{API_BASE}/repos/{REPO}/git/refs"
|
||||
payload = {"ref": f"refs/heads/{new_branch}", "sha": from_sha}
|
||||
r = requests.post(url, json=payload, headers=api_headers())
|
||||
if r.status_code in (201, 422):
|
||||
print(f"Branch {new_branch} created or already exists.")
|
||||
return True
|
||||
else:
|
||||
die(f"Failed to create branch: {r.status_code} {r.text}")
|
||||
|
||||
def get_file_sha(path, branch):
|
||||
url = f"{API_BASE}/repos/{REPO}/contents/{quote_plus(path)}?ref={branch}"
|
||||
r = requests.get(url, headers=api_headers())
|
||||
if r.status_code == 200:
|
||||
return r.json().get("sha")
|
||||
return None
|
||||
|
||||
def put_file(path, content_b64, message, branch, sha=None):
|
||||
url = f"{API_BASE}/repos/{REPO}/contents/{quote_plus(path)}"
|
||||
payload = {"message": message, "content": content_b64, "branch": branch}
|
||||
if sha:
|
||||
payload["sha"] = sha
|
||||
r = requests.put(url, json=payload, headers=api_headers())
|
||||
return (r.status_code in (200,201)), r.text
|
||||
|
||||
def extract_zip(zip_path, target_dir):
|
||||
with zipfile.ZipFile(zip_path, 'r') as z:
|
||||
z.extractall(target_dir)
|
||||
|
||||
def gather_files(root_dir):
|
||||
files = []
|
||||
for dirpath, dirnames, filenames in os.walk(root_dir):
|
||||
if ".git" in dirpath.split(os.sep):
|
||||
continue
|
||||
for fn in filenames:
|
||||
files.append(os.path.join(dirpath, fn))
|
||||
return files
|
||||
|
||||
def main():
|
||||
if not GITHUB_TOKEN or not REPO or not ZIP_FILENAME:
|
||||
print("Set env vars: GITHUB_TOKEN, REPO, ZIP_FILENAME. Optionally BRANCH.")
|
||||
sys.exit(1)
|
||||
if not os.path.exists(ZIP_FILENAME):
|
||||
die(f"Zip file not found: {ZIP_FILENAME}")
|
||||
default_branch = get_default_branch()
|
||||
print("Default branch:", default_branch)
|
||||
base_sha = get_ref_sha(default_branch)
|
||||
if not base_sha:
|
||||
die(f"Could not find ref for default branch {default_branch}")
|
||||
create_branch(BRANCH, base_sha)
|
||||
tmp_dir = Path("tmp_upload")
|
||||
if tmp_dir.exists():
|
||||
for p in tmp_dir.rglob("*"):
|
||||
try:
|
||||
if p.is_file(): p.unlink()
|
||||
except: pass
|
||||
tmp_dir.mkdir(exist_ok=True)
|
||||
print("Extracting zip...")
|
||||
extract_zip(ZIP_FILENAME, str(tmp_dir))
|
||||
files = gather_files(str(tmp_dir))
|
||||
print(f"Found {len(files)} files to upload")
|
||||
uploaded = 0
|
||||
for fpath in files:
|
||||
rel = os.path.relpath(fpath, str(tmp_dir))
|
||||
rel_posix = Path(rel).as_posix()
|
||||
with open(fpath, "rb") as fh:
|
||||
data = fh.read()
|
||||
content_b64 = base64.b64encode(data).decode("utf-8")
|
||||
sha = get_file_sha(rel_posix, BRANCH)
|
||||
msg = f"Add/update {rel_posix} via uploader"
|
||||
ok, resp = put_file(rel_posix, content_b64, msg, BRANCH, sha=sha)
|
||||
if ok:
|
||||
uploaded += 1
|
||||
print(f"[{uploaded}/{len(files)}] Uploaded: {rel_posix}")
|
||||
else:
|
||||
print(f"[!] Failed: {rel_posix} - {resp}")
|
||||
time.sleep(0.25)
|
||||
print(f"Completed. Uploaded {uploaded} files to branch {BRANCH}.")
|
||||
print(f"Open PR: https://github.com/{REPO}/compare/{BRANCH}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
26
upload_repo_diag.py
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
import os, sys
|
||||
from pathlib import Path
|
||||
|
||||
def env(k):
|
||||
v = os.environ.get(k)
|
||||
return "<SET>" if v else "<NOT SET>"
|
||||
|
||||
print("Python:", sys.version.splitlines()[0])
|
||||
print("PWD:", os.getcwd())
|
||||
print("Workspace files:")
|
||||
for p in Path(".").iterdir():
|
||||
print(" -", p)
|
||||
|
||||
print("\nImportant env vars:")
|
||||
for k in ("GITHUB_TOKEN","REPO","BRANCH","ZIP_FILENAME"):
|
||||
print(f" {k}: {env(k)}")
|
||||
|
||||
print("\nAttempting to read ZIP_FILENAME if set...")
|
||||
zipf = os.environ.get("ZIP_FILENAME")
|
||||
if zipf:
|
||||
p = Path(zipf)
|
||||
print("ZIP path:", p.resolve())
|
||||
print("Exists:", p.exists(), "Size:", p.stat().st_size if p.exists() else "N/A")
|
||||
else:
|
||||
print("ZIP_FILENAME not set; cannot check file.")
|
||||