Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: "CybICS CodeQL Configuration"

paths-ignore:
# Third-party: OpenPLC v3 (upstream project, not our code)
- software/OpenPLC/OpenPLC_v3
# Virtual environments
- "**/.venv"
- "**/node_modules"
1 change: 1 addition & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
config-file: .github/codeql/codeql-config.yml

- name: Autobuild
uses: github/codeql-action/autobuild@v3
Expand Down
48 changes: 28 additions & 20 deletions software/cybicsagent/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ def get_container_status():
except subprocess.TimeoutExpired:
return {'error': 'Command timed out'}
except Exception as e:
return {'error': str(e)}
logger.error(f"Error getting container status: {e}")
return {'error': 'Failed to get container status'}


def restart_containers(container_names=None):
Expand Down Expand Up @@ -125,7 +126,8 @@ def restart_containers(container_names=None):
except subprocess.TimeoutExpired:
return {'error': 'Restart operation timed out'}
except Exception as e:
return {'error': str(e)}
logger.error(f"Error restarting containers: {e}")
return {'error': 'Failed to restart containers'}


def get_container_logs(container_name, lines=50):
Expand Down Expand Up @@ -154,7 +156,8 @@ def get_container_logs(container_name, lines=50):
except subprocess.TimeoutExpired:
return {'error': 'Command timed out'}
except Exception as e:
return {'error': str(e)}
logger.error(f"Error getting container logs: {e}")
return {'error': 'Failed to get container logs'}


def get_system_stats():
Expand Down Expand Up @@ -186,7 +189,8 @@ def get_system_stats():
except subprocess.TimeoutExpired:
return {'error': 'Command timed out'}
except Exception as e:
return {'error': str(e)}
logger.error(f"Error getting system stats: {e}")
return {'error': 'Failed to get system statistics'}


def execute_network_scan(target, scan_type='basic'):
Expand Down Expand Up @@ -227,7 +231,8 @@ def execute_network_scan(target, scan_type='basic'):
except FileNotFoundError:
return {'error': 'nmap not found - network scanning not available'}
except Exception as e:
return {'error': str(e)}
logger.error(f"Error executing network scan: {e}")
return {'error': 'Failed to execute network scan'}


def list_docker_images():
Expand Down Expand Up @@ -259,7 +264,8 @@ def list_docker_images():
except subprocess.TimeoutExpired:
return {'error': 'Command timed out'}
except Exception as e:
return {'error': str(e)}
logger.error(f"Error listing Docker images: {e}")
return {'error': 'Failed to list Docker images'}


# Tool definitions for the LLM
Expand Down Expand Up @@ -340,9 +346,11 @@ def execute_tool(tool_name, parameters=None):
result = func()
return result
except TypeError as e:
return {'error': f'Invalid parameters for {tool_name}: {str(e)}'}
logger.error(f"Invalid parameters for {tool_name}: {e}")
return {'error': f'Invalid parameters for {tool_name}'}
except Exception as e:
return {'error': f'Error executing {tool_name}: {str(e)}'}
logger.error(f"Error executing {tool_name}: {e}")
return {'error': f'Error executing {tool_name}'}


def parse_tool_calls(response_text):
Expand Down Expand Up @@ -714,7 +722,7 @@ def generate_response(question, context):

except Exception as e:
logger.error(f"Error generating response: {e}")
return f"⚠️ **Error**: I'm having trouble connecting to the AI model.\n\n`{str(e)}`"
return "⚠️ **Error**: I'm having trouble connecting to the AI model. Please check the model configuration."


@app.route('/health', methods=['GET'])
Expand Down Expand Up @@ -757,7 +765,7 @@ def detect_tool_intent(question):
if container in question_lower:
# Check for line count
import re
lines_match = re.search(r'(\d+)\s*lines?', question_lower)
lines_match = re.search(r'(\d+)\s{0,5}lines?', question_lower[:200])
lines = int(lines_match.group(1)) if lines_match else 50
return (True, 'get_container_logs', {'container_name': container, 'lines': lines})
return (False, None, {})
Expand Down Expand Up @@ -928,7 +936,7 @@ def chat():

except Exception as e:
logger.error(f"Error in chat endpoint: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
return jsonify({'error': 'An internal error occurred while processing your message'}), 500


@app.route('/api/info', methods=['GET'])
Expand Down Expand Up @@ -963,7 +971,7 @@ def info():
})
except Exception as e:
logger.error(f"Error in info endpoint: {e}")
return jsonify({'error': str(e)}), 500
return jsonify({'error': 'An internal error occurred while retrieving agent information'}), 500


@app.route('/api/tools', methods=['GET'])
Expand All @@ -984,7 +992,7 @@ def list_tools():
})
except Exception as e:
logger.error(f"Error listing tools: {e}")
return jsonify({'error': str(e)}), 500
return jsonify({'error': 'An internal error occurred while listing tools'}), 500


@app.route('/api/model', methods=['GET'])
Expand Down Expand Up @@ -1039,7 +1047,7 @@ def get_model():
})
except Exception as e:
logger.error(f"Error getting model info: {e}")
return jsonify({'error': str(e)}), 500
return jsonify({'error': 'An internal error occurred while retrieving model information'}), 500


@app.route('/api/model', methods=['POST'])
Expand Down Expand Up @@ -1112,25 +1120,25 @@ def set_model():
logger.error(f"Model downloaded but failed to activate: {test_error}")
return jsonify({
'success': False,
'error': f'Model downloaded but failed to activate: {str(test_error)}'
'error': 'Model downloaded but failed to activate'
}), 500

except Exception as pull_error:
logger.error(f"Error pulling model {new_model}: {pull_error}")
return jsonify({
'success': False,
'error': f'Failed to download model: {str(pull_error)}'
'error': 'Failed to download the requested model'
}), 500
else:
logger.error(f"Error setting model: {e}")
return jsonify({
'success': False,
'error': str(e)
'error': 'Failed to set the specified model'
}), 500

except Exception as e:
logger.error(f"Error in set_model endpoint: {e}")
return jsonify({'error': str(e)}), 500
return jsonify({'error': 'An internal error occurred while setting the model'}), 500


@app.route('/api/model/pull', methods=['POST'])
Expand All @@ -1157,12 +1165,12 @@ def pull_model():
logger.error(f"Error pulling model {model_name}: {e}")
return jsonify({
'success': False,
'error': str(e)
'error': 'Failed to download the specified model'
}), 500

except Exception as e:
logger.error(f"Error in pull_model endpoint: {e}")
return jsonify({'error': str(e)}), 500
return jsonify({'error': 'An internal error occurred while pulling the model'}), 500


if __name__ == '__main__':
Expand Down
71 changes: 43 additions & 28 deletions software/landing/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
CybICS - Industrial Control Systems Training Platform
Main Flask Application (Refactored)
"""
from flask import Flask, render_template, jsonify, request, session, send_from_directory
from flask import Flask, render_template, jsonify, request, session, send_from_directory, abort
from werkzeug.utils import safe_join
import os
import sys

Expand Down Expand Up @@ -111,7 +112,7 @@
return jsonify(stats)
except Exception as e:
logger.error(f"Error getting stats: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
return jsonify({'error': 'Internal server error'}), 500

@app.route('/api/stats/history')
def get_stats_history():
Expand All @@ -122,7 +123,7 @@
return jsonify(history)
except Exception as e:
logger.error(f"Error getting stats history: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
return jsonify({'error': 'Internal server error'}), 500

# ========== NETWORK ANALYZER ROUTES ==========

Expand Down Expand Up @@ -156,9 +157,9 @@
logger.info(f'Executing webshell command: {command}')

try:
# Execute the command without timeout
# Intentional: webshell provides command execution for CTF training
result = subprocess.run(
command,
command, # nosec - intentional command execution for CTF webshell
Comment thread Dismissed
shell=True,
capture_output=True,
text=True,
Expand All @@ -178,7 +179,7 @@
logger.error(f'Error executing command: {e}', exc_info=True)
return jsonify({
'success': False,
'output': f'Error: {str(e)}'
'output': 'Internal server error'
}), 500

# ========== CTF ROUTES ==========
Expand Down Expand Up @@ -310,28 +311,42 @@
@app.route('/ctf/challenge/<challenge_id>/<path:filename>')
def serve_challenge_asset(challenge_id, filename):
"""Serve challenge assets like images"""
training_dir = os.path.join(TRAINING_DIR, challenge_id)
return send_from_directory(training_dir, filename)
safe_challenge_dir = safe_join(TRAINING_DIR, challenge_id)
if safe_challenge_dir is None:
abort(400)
resolved = os.path.realpath(safe_challenge_dir)
if not resolved.startswith(os.path.realpath(TRAINING_DIR)):
abort(400)
return send_from_directory(resolved, filename)

@app.route('/ctf/challenge/doc/<path:filename>')
def serve_challenge_doc_asset(filename):
"""Serve challenge doc assets like images from doc/ folder"""
# Sanitize filename to prevent directory traversal
safe_filename = os.path.basename(filename)

# Check the referer header to determine which challenge we're in
referer = request.headers.get('Referer', '')
if '/ctf/challenge/' in referer:
challenge_id = referer.split('/ctf/challenge/')[-1].split('/')[0].split('?')[0]
doc_path = os.path.join(TRAINING_DIR, challenge_id, 'doc')
try:
return send_from_directory(doc_path, filename)
except:
pass
safe_doc_path = safe_join(TRAINING_DIR, challenge_id, 'doc')
if safe_doc_path is not None:
resolved = os.path.realpath(safe_doc_path)
if resolved.startswith(os.path.realpath(TRAINING_DIR)):
try:
return send_from_directory(resolved, safe_filename)
except:
pass

# Fallback: try to find the file in any training directory's doc folder
for challenge_dir in os.listdir(TRAINING_DIR):
doc_path = os.path.join(TRAINING_DIR, challenge_dir, 'doc')
if os.path.isdir(doc_path):
resolved = os.path.realpath(doc_path)
if not resolved.startswith(os.path.realpath(TRAINING_DIR)):
continue
try:
return send_from_directory(doc_path, filename)
return send_from_directory(resolved, safe_filename)
except:
continue

Expand Down Expand Up @@ -474,8 +489,8 @@
logger.error('Docker compose logs command timed out')
return jsonify({'error': 'Logs generation timed out'}), 500
except Exception as e:
logger.error(f'Error generating logs: {str(e)}')
return jsonify({'error': str(e)}), 500
logger.error(f'Error generating logs: {str(e)}', exc_info=True)
return jsonify({'error': 'Internal server error'}), 500

@app.route('/api/settings/system/info')
def system_info():
Expand Down Expand Up @@ -517,8 +532,8 @@
'running_containers': container_count
})
except Exception as e:
logger.error(f'Error getting system info: {str(e)}')
return jsonify({'error': str(e)}), 500
logger.error(f'Error getting system info: {str(e)}', exc_info=True)
return jsonify({'error': 'Internal server error'}), 500

@app.route('/api/settings/containers/restart', methods=['POST'])
def restart_containers():
Expand Down Expand Up @@ -606,8 +621,8 @@
logger.error('Container restart timed out')
return jsonify({'error': 'Restart operation timed out'}), 500
except Exception as e:
logger.error(f'Error restarting containers: {str(e)}')
return jsonify({'error': str(e)}), 500
logger.error(f'Error restarting containers: {str(e)}', exc_info=True)
return jsonify({'error': 'Internal server error'}), 500

@app.route('/api/settings/agent', methods=['GET', 'POST'])
def agent_settings():
Expand Down Expand Up @@ -659,8 +674,8 @@
logger.error('Could not connect to agent service')
return jsonify({'error': 'Agent service unavailable'}), 503
except Exception as e:
logger.error(f'Error communicating with agent: {str(e)}')
return jsonify({'error': str(e)}), 500
logger.error(f'Error communicating with agent: {str(e)}', exc_info=True)
return jsonify({'error': 'Internal server error'}), 500

@app.route('/api/agent/status', methods=['GET'])
def agent_status():
Expand Down Expand Up @@ -725,8 +740,8 @@
logger.error('Could not connect to agent service')
return jsonify({'error': 'Agent service unavailable'}), 503
except Exception as e:
logger.error(f'Error getting agent model: {str(e)}')
return jsonify({'error': str(e)}), 500
logger.error(f'Error getting agent model: {str(e)}', exc_info=True)
return jsonify({'error': 'Internal server error'}), 500

@app.route('/api/agent/model', methods=['POST'])
def set_agent_model():
Expand Down Expand Up @@ -760,8 +775,8 @@
logger.error('Could not connect to agent service')
return jsonify({'error': 'Agent service unavailable'}), 503
except Exception as e:
logger.error(f'Error setting agent model: {str(e)}')
return jsonify({'error': str(e)}), 500
logger.error(f'Error setting agent model: {str(e)}', exc_info=True)
return jsonify({'error': 'Internal server error'}), 500

@app.route('/api/agent/model/pull', methods=['POST'])
def pull_agent_model():
Expand Down Expand Up @@ -793,8 +808,8 @@
logger.error('Could not connect to agent service')
return jsonify({'error': 'Agent service unavailable'}), 503
except Exception as e:
logger.error(f'Error pulling agent model: {str(e)}')
return jsonify({'error': str(e)}), 500
logger.error(f'Error pulling agent model: {str(e)}', exc_info=True)
return jsonify({'error': 'Internal server error'}), 500

# ========== APPLICATION ENTRY POINT ==========

Expand Down
Loading
Loading