def unit_signup(unit_id): unit = ds_client.get(ds_client.key('cybergym-unit', unit_id)) if request.method == 'POST': workout_query = ds_client.query(kind='cybergym-workout') workout_query.add_filter('unit_id', '=', unit_id) claimed_workout = None for workout in list(workout_query.fetch()): if 'student_name' in workout: if workout['student_name'] == None or workout[ 'student_name'] == "": with ds_client.transaction(): claimed_workout = workout claimed_workout['student_name'] = request.form[ 'student_name'] ds_client.put(claimed_workout) if unit['build_type'] == 'arena': return redirect('/student/arena_landing/%s' % claimed_workout.key.name) else: return redirect('/student/landing/%s' % claimed_workout.key.name) else: with ds_client.transaction(): claimed_workout = workout claimed_workout['student_name'] = request.form[ 'student_name'] ds_client.put(claimed_workout) if unit['build_type'] == 'arena': return redirect('/student/arena_landing/%s' % claimed_workout.key.name) else: return redirect('/student/landing/%s' % claimed_workout.key.name) return render_template('unit_signup.html', unit_full=True) return render_template('unit_signup.html')
def registered_student_home(): if 'user_email' in session and 'user_groups' in session: student_email = session['user_email'] if ArenaAuthorizer.UserGroups.STUDENTS not in session['user_groups']: return render_template('403.html') workout_list = ds_client.query(kind='cybergym-workout') workout_list.add_filter("student_email", "=", str(student_email)) student_workouts = [] for workout in list(workout_list.fetch()): if workout['state'] != BUILD_STATES.DELETED: workout_info = { 'workout_id': workout.key.name, 'workout_name': workout['type'], 'timestamp': workout['timestamp'] } student_workouts.append(workout_info) student_workouts = sorted(student_workouts, key=lambda i: (i['timestamp']), reverse=True) student_info = {'workouts': student_workouts} return render_template('student_home.html', auth_config=auth_config, student_info=student_info) else: return render_template('login.html', auth_config=auth_config)
def get_unit_arenas(unit_id): unit_workouts = ds_client.query(kind='cybergym-workout') unit_workouts.add_filter("unit_id", "=", unit_id) workout_list = [] for workout in list(unit_workouts.fetch()): student_name = None if 'student_name' in workout: student_name = workout['student_name'] workout_instance = workout = ds_client.get(workout.key) state = None if 'state' in workout_instance: state = workout_instance['state'] team = None if 'team' in workout_instance: team = workout_instance['team'] workout_info = { 'name': workout.key.name, 'student_name': student_name, 'state': state, 'teacher_email': workout['instructor_id'], 'team': team } workout_list.append(workout_info) return workout_list
def build_arena(self): """ @return: Status @rtype: bool """ self._get_build_allocation() if not self.allocation: # If there are not enough resource, delete the unit and workouts from the datastore before returning. ds_client.delete(self.unit_id) for workout in self.workouts: ds_client.delete(workout.key) servers = ds_client.query(kind='cybergym-server').add_filter( 'workout', '=', workout.key.name).fetch() for server in servers: ds_client.delete(server) servers = ds_client.query(kind='cybergym-server').add_filter( 'workout', '=', workout.key.name).fetch() for server in servers: ds_client.delete(server) return False _projects = list(self.allocation.keys()) i = 0 j = 0 publisher = topic_path = current_project = None for workout in self.workouts: if i == 0: current_project = _projects[j] j += 1 publisher = pubsub_v1.PublisherClient() topic_path = publisher.topic_path( current_project, workout_globals.ps_build_workout_topic) workout['build_project_location'] = current_project ds_client.put(workout) publisher.publish(topic_path, data=b'Cyber Gym Workout', workout_id=workout.key.name) i += 1 # If the index is greater than the current project's build allocation, reset the index, which triggers # a project change on the next iteration. if i >= self.allocation[current_project]: i = 0 return True
def __init__(self, unit_id): self.allocation = {} self.unit_id = unit_id self.unit = ds_client.get(ds_client.key('cybergym-unit', unit_id)) unit_workouts = ds_client.query(kind='cybergym-workout') unit_workouts.add_filter("unit_id", "=", unit_id) self.workouts = list(unit_workouts.fetch()) self.workout_count = len(self.workouts) self.remaining_count = self.workout_count
def _process_class_roster(self): class_list = ds_client.query(kind='cybergym-class') class_list.add_filter('teacher_email', '=', self.email) class_list.add_filter('class_name', '=', self.class_name) self.class_record = list(class_list.fetch())[0] for student_name in self.class_record['roster']: if self.class_record['student_auth'] == 'email': self.student_emails.append(student_name['student_email']) self.student_names.append(student_name)
def admin_workout(workout_id): workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) if workout: workout_server_query = ds_client.query(kind='cybergym-server') workout_server_query.add_filter('workout', '=', workout_id) server_list = [] for server in list(workout_server_query.fetch()): server_list.append(server) return render_template('admin_workout.html', workout=workout, servers=server_list) else: abort(404)
def _get_build_allocation(self): """ Identify the build allocation among the parent and all children based on available resources and identified quotas. @param workout_count: Number of workouts to build in this request @type workout_count: Integer @return: The parent/or children projects to use, which may split across multiple projects @rtype: Dict {'<parent>': 80, '<child1>': 300, '<child2>': 300} """ # First, get all available projects. available = {project: self.MAX_BUILDS} admin_info = ds_client.get( ds_client.key(AdminInfoEntity.KIND, 'cybergym')) if AdminInfoEntity.Entities.CHILD_PROJECTS in admin_info: for child_project in admin_info[ AdminInfoEntity.Entities.CHILD_PROJECTS]: available[child_project] = self.MAX_BUILDS # Next find all active workouts for the application query_workouts = ds_client.query(kind='cybergym-workout') query_workouts.add_filter('active', '=', True) # Determine the available build left for each parent and children projects. for workout in list(query_workouts.fetch()): workout_project = workout.get('build_project_location', project) if workout_project not in available: cloud_log( LogIDs.MAIN_APP, f"Error in workout specification. The project {workout_project} is not " f"a valid project for this application.", LOG_LEVELS.ERROR) return False available[workout_project] -= 1 # Set the build allocation for this project based on availability. for _project in available: workouts_available = available[_project] if workouts_available > 0: _project_allocation = min(self.remaining_count, workouts_available) self.allocation[_project] = _project_allocation self.remaining_count -= _project_allocation if self.remaining_count > 0: cloud_log( LogIDs.MAIN_APP, "Error: Not enough available resources to complete the build!", LOG_LEVELS.ERROR) return False else: return True
def get_unit_workouts(unit_id): unit_workouts = ds_client.query(kind='cybergym-workout') unit_workouts.add_filter("unit_id", "=", unit_id) workout_list = [] for workout in list(unit_workouts.fetch()): workout_instance = workout = ds_client.get(workout.key) workout_info = { 'name': workout.key.name, # 'running': workout_instance['running'], 'complete': workout_instance['complete'], } workout_list.append(workout_info) return workout_list
def get_active_workouts(): active_workout_query = ds_client.query(kind='cybergym-workout') workout_list = active_workout_query.fetch() active_workouts = [] for workout in workout_list: if 'state' in workout: if workout['state'] != "DELETED" and workout[ 'state'] != "COMPLETED_DELETING_SERVERS": if 'hourly_cost' in workout and 'runtime_counter' in workout: if workout['hourly_cost'] and workout['runtime_counter']: estimated_cost = (float(workout['hourly_cost']) / 3600 ) * float(workout['runtime_counter']) workout['estimated_cost'] = format( estimated_cost, '.2f') workout['id'] = workout.key.name active_workouts.append(workout) return json.dumps(active_workouts)
def get_teacher_info(): if (request.method == "POST"): teacher_email = request.get_json() unit_list = ds_client.query(kind='cybergym-unit') unit_list.add_filter("instructor_id", "=", str(teacher_email['teacher_email'])) # unit_list.add_filter("resources_deleted", "=", False) teacher_units = [] for unit in list(unit_list.fetch()): unit_info = { 'unit_id': unit.key.name, 'workout_name': unit['workout_name'], 'build_type': unit['build_type'], 'unit_name': unit['unit_name'], 'timestamp': unit['timestamp'] } teacher_units.append(unit_info) teacher_units = sorted(teacher_units, key=lambda i: (i['timestamp']), reverse=True) return json.dumps(teacher_units)
def get_unit_workouts(unit_id): unit_workouts = ds_client.query(kind='cybergym-workout') unit_workouts.add_filter("unit_id", "=", unit_id) workout_list = [] for workout in list(unit_workouts.fetch()): student_name = None if 'student_name' in workout: student_name = workout['student_name'] if not student_name: student_name = "" # workout_instance = workout = ds_client.get(workout.key) state = None if 'state' in workout: state = workout['state'] submitted_answers = None if 'submitted_answers' in workout: submitted_answers = workout['submitted_answers'] uploaded_files = None if 'uploaded_files' in workout: uploaded_files = workout['uploaded_files'] assessment = workout.get('assessment', None) auto_answers = get_auto_assessment(workout) workout_info = { 'name': workout.key.name, 'student_name': student_name, 'state': state, 'submitted_answers': submitted_answers, 'uploaded_files': uploaded_files, 'auto_answers': auto_answers, 'assessment': assessment } workout_list.append(workout_info) return workout_list
def _submit_flag(self, submission): """ Handles flag submission for Arena CTF challenges. Calculates score based on value provided in the Arena yaml, with a bonus awarded to the first team to submit. Also marks completion for all team members. @param: submission: Dict generated from form submission """ points = 0 response = {} answer_found = False flag_attempt = submission['answer'] question_key = submission['question'] point_value = 0 unit = ds_client.get( ds_client.key('cybergym-unit', self.workout['unit_id'])) for i in range(len(self.assessment_info['questions'])): if question_key == self.assessment_info['questions'][i].get( 'question') and flag_attempt == self.assessment_info[ 'questions'][i].get('answer'): point_value = self.assessment_info['questions'][i].get( 'points') answer_found = True # if flag_attempt in valid_answers: if answer_found: answer_time = time.gmtime(time.time()) time_string = str(answer_time[3]) + ":" + str( answer_time[4]) + ":" + str(answer_time[5]) team_members = [] if 'team' in self.workout: team_query = ds_client.query(kind='cybergym-workout') team_query.add_filter('team', '=', self.workout['team']) team_query.add_filter('unit_id', '=', self.workout['unit_id']) for team_member in list(team_query.fetch()): team_members.append(team_member) answer_time_dict = { 'answer': flag_attempt, 'timestamp': time_string } #check if this is the first time this flag has been found if 'found_flags' in unit: if flag_attempt not in unit['found_flags']: unit['found_flags'].append(flag_attempt) point_value += 50 ds_client.put(unit) answer_time_dict['first'] = True else: unit['found_flags'] = [] unit['found_flags'].append(flag_attempt) point_value += 50 answer_time_dict['first'] = True ds_client.put(unit) #check if the answer has already been submitted by another team member if self.submitted_answers: if flag_attempt not in self.workout['submitted_answers']: self.submitted_answers.append(answer_time_dict) else: response = { 'answer_correct': True, 'points_gained': 0, } return response else: self.submitted_answers = [] self.submitted_answers.append(answer_time_dict) self.workout['submitted_answers'] = self.submitted_answers points += int(point_value) response = { 'answer_correct': True, 'points_gained': points, } #Add submitted answer to team member entities if 'team' in self.workout: for member in team_members: if 'points' in member: prev_score = member['points'] prev_score += point_value member['points'] = prev_score else: member['points'] = point_value member['submitted_answers'] = self.submitted_answers ds_client.put(member) else: if 'points' in self.workout: prev_score = self.workout['points'] prev_score += point_value self.workout['points'] = prev_score else: self.workout['points'] = point_value ds_client.put(self.workout) else: response = {'answer_correct': False, 'points_gained': 0} return response
def admin_page(): admin_info = ds_client.get(ds_client.key('cybergym-admin-info', 'cybergym')) comment_query = ds_client.query(kind='cybergym-comments') comment_query.order = ['date'] comment_list = comment_query.fetch() comments = [] active_workouts = [] for comment in comment_list: comments.append(comment) instance_list = [] machine_instances = compute.instances().list(project=project, zone=zone, filter=("name:cybergym-*")).execute() if 'items' in machine_instances: for instance in machine_instances['items']: if 'name' in instance: instance_list.append(str(instance['name'])) if request.method == "POST": response = { 'action_completed': 'false' } data = request.get_json(force=True) if data['action'] == 'approve_user': if data['user_email'] in admin_info['pending']: admin_info['authorized_users'].append(data['user_email']) admin_info['pending'].remove(data['user_email']) ds_client.put(admin_info) response['action_completed'] = 'true' g_logger = log_client.logger('admin-app') g_logger.log_struct( { "message": "User {} approved for general access".format(data['user_email']) }, severity=LOG_LEVELS.INFO ) elif data['action'] == 'deny_user': if data['user_email'] in admin_info['pending']: admin_info['pending'].remove(data['user_email']) ds_client.put(admin_info) response['action_completed'] = 'true' g_logger = log_client.logger('admin-app') g_logger.log_struct( { "message": "User {} denied general access".format(data['user_email']) }, severity=LOG_LEVELS.INFO ) elif data['action'] == 'promote_user': if data['user_email'] in admin_info['authorized_users']: admin_info['admins'].append(data['user_email']) ds_client.put(admin_info) response['action_completed'] = 'true' g_logger = log_client.logger('admin-app') g_logger.log_struct( { "message": "User {} granted administrator privileges".format(data['user_email']) }, severity=LOG_LEVELS.INFO ) elif data['action'] == 'remove_user': if data['user_email'] in admin_info['authorized_users']: admin_info['authorized_users'].remove(data['user_email']) ds_client.put(admin_info) response['action_completed'] = 'true' g_logger = log_client.logger('admin-app') g_logger.log_struct( { "message": "User {} removed".format(data['user_email']) }, severity=LOG_LEVELS.INFO ) elif data['action'] == 'revoke_access': if data['user_email'] in admin_info['admins']: admin_info['admins'].remove(data['user_email']) ds_client.put(admin_info) response['action_completed'] = 'true' g_logger = log_client.logger('admin-app') g_logger.log_struct( { "message": "User {} is no longer an administrator".format(data['user_email']) }, severity=LOG_LEVELS.INFO ) return json.dumps(response) admin_actions = {key: value for key, value in AdminActions.__dict__.items() if not (key.startswith('__') and key.endswith('__'))} # print(admin_actions) return render_template('admin_page.html', auth_config=auth_config, admin_info=admin_info, admin_actions=admin_actions, active_workouts=active_workouts, comments=comments, instance_list=json.dumps(instance_list))
def landing_page(workout_id): unit_list = ds_client.query(kind='cybergym-unit') workouts_list = list( unit_list.add_filter('workouts', '=', str(workout_id)).fetch()) if not workouts_list: return render_template('404.html') else: workout = ds_client.get(ds_client.key('cybergym-workout', workout_id)) unit = ds_client.get(ds_client.key('cybergym-unit', workout['unit_id'])) admin_info = ds_client.get( ds_client.key('cybergym-admin-info', 'cybergym')) registration_required = workout.get('registration_required', False) logged_in_user = session.get('user_email', None) registered_user = workout.get('student_email', None) instructor_user = unit.get('instructor_id', None) allowed_users = admin_info['admins'].copy() + [instructor_user ] + [registered_user] workout_server_query = ds_client.query(kind='cybergym-server') workout_server_query.add_filter('workout', '=', workout_id) server_list = [] survey_yaml = parse_survey_yaml() survey = survey_yaml.get('survey', None) if survey_yaml else None for server in list(workout_server_query.fetch()): server_name = server['name'][11:] server['name'] = server_name try: snapshots = compute.snapshots().list( project=project, filter=f"name = {server.key.name}*").execute() server['snapshots'] = [] for snapshot in snapshots['items']: server['snapshots'].append({ 'snapshot_name': snapshot['name'], 'creation_date': snapshot['creationTimestamp'] }) except Exception as e: pass server_list.append(server) if registration_required and logged_in_user not in allowed_users: return render_template('login.html', auth_config=auth_config) if workout: assessment_manager = CyberArenaAssessment(workout_id) expiration = None is_expired = True if 'expiration' in workout: if (int(time.time()) - (int(workout['timestamp']) + ((int(workout['expiration'])) * 60 * 60 * 24))) < 0: is_expired = False expiration = time.strftime( '%d %B %Y', (time.localtime((int(workout['expiration']) * 60 * 60 * 24) + int(workout['timestamp'])))) shutoff = None if 'run_hours' in workout: run_hours = int(workout['run_hours']) if run_hours == 0: shutoff = "expired" else: shutoff = time.strftime( '%d %B %Y at %I:%M %p', (time.localtime((int(workout['run_hours']) * 60 * 60) + int(workout['start_time'])))) build_type = unit['build_type'] container_url = None if build_type == 'container': container_url = workout['container_url'] assessment = None if 'assessment' in workout and workout['assessment']: assessment = assessment_manager.get_assessment_questions() build_now = unit.get('build_now', True) if (request.method == "POST"): attempt = assessment_manager.submit() return json.dumps(attempt) return render_template('landing_page.html', build_type=build_type, workout=workout, description=unit['description'], container_url=container_url, dns_suffix=dns_suffix, expiration=expiration, shutoff=shutoff, assessment=assessment, is_expired=is_expired, build_now=build_now, auth_config=auth_config, servers=server_list, survey=survey) else: return render_template('no_workout.html')
def process_assessment(workout, workout_id, request, assessment): valid_answers = [] if workout['type'] == 'arena': points = 0 response = {} answer_found = False flag_attempt = request.form.get('answer') question_key = request.form.get('question_content') point_value = int(request.form.get('point_value')) unit = ds_client.get(ds_client.key('cybergym-unit', workout['unit_id'])) for i in range(len(assessment)): if question_key == assessment[i].get( 'question') and flag_attempt == assessment[i].get( 'answer'): answer_found = True # if flag_attempt in valid_answers: if answer_found: answer_time = time.gmtime(time.time()) time_string = str(answer_time[3]) + ":" + str( answer_time[4]) + ":" + str(answer_time[5]) team_query = ds_client.query(kind='cybergym-workout') team_query.add_filter('teacher_email', '=', workout['teacher_email']) team_query.add_filter('unit_id', '=', workout['unit_id']) team_members = [] for team_member in list(team_query.fetch()): team_members.append(team_member) answer_time_dict = { 'answer': flag_attempt, 'timestamp': time_string } #check if the answer has already been submitted by another team member if 'submitted_answers' in workout: if flag_attempt not in workout['submitted_answers']: workout['submitted_answers'].append(answer_time_dict) else: response = { 'answer_correct': True, 'points_gained': 0, } return json.dumps(response) else: workout['submitted_answers'] = [] workout['submitted_answers'].append(answer_time_dict) points += int(point_value) response = { 'answer_correct': True, 'points_gained': points, } #check if this is the first time this flag has been found if 'found_flags' in unit: if flag_attempt in unit['found_flags']: print("flag already found") else: unit['found_flags'].append(flag_attempt) point_value += 50 ds_client.put(unit) else: unit['found_flags'] = [] unit['found_flags'].append(flag_attempt) point_value += 50 ds_client.put(unit) for member in team_members: if 'points' in member: prev_score = member['points'] prev_score += point_value member['points'] = prev_score else: member['points'] = point_value member['submitted_answers'] = workout['submitted_answers'] ds_client.put(member) else: if unit['found_flags']: if flag_attempt in unit['found_flags']: response = {'answer_correct': True, 'points_gained': 0} return json.dumps(response) else: response = {'answer_correct': False, 'points_gained': 0} return json.dumps(response) return json.dumps(response) else: num_correct = 0 for i in range(len(assessment)): if (assessment[i].get('type') != 'upload'): valid_answers.append(assessment[i].get('answer')) assessment_answers = request.form.getlist('answer') assessment_questions = request.form.getlist('question') assessment_upload_prompt = request.form.getlist('upload_prompt') assessment_uploads = request.files.getlist('file_upload') store_student_uploads(workout_id, assessment_uploads) for i in range(len(assessment_answers)): user_input = { "question": assessment_questions[i], "answer": assessment_answers[i] } user_answer = str(user_input['answer']) true_answer = str(assessment[i].get('answer')) if valid_answers[i]: if (user_answer.lower() == valid_answers[i].lower()): num_correct += 1 uploaded_files = [] urls = retrieve_student_uploads(workout_id) for index, item in enumerate(assessment_uploads): user_input = { "question": assessment_upload_prompt[index], "storage_url": urls[index] } uploaded_files.append(user_input) percentage_correct = num_correct / len(assessment_questions) * 100 workout['uploaded_files'] = uploaded_files workout['submitted_answers'] = assessment_answers workout['assessment_score'] = percentage_correct ds_client.put(workout) return uploaded_files, assessment_answers, percentage_correct
def __init__(self, server_spec, build_id, startup_scripts=None, student_entry=False, options=None): """ Initialize the server specification from a build specification. @param server_spec: The raw build specification for the server. @type server_spec: dict @param build_id: The ID associated with the build @type build_id: str @param startup_scripts: The startup scripts for all servers associated with this build @type startup_scripts: dict @param student_entry: Whether this is a student entry server @type: bool @param options: Additional options to include with the server @type options: dict """ base_name = server_spec['name'] self.server_name = f"{build_id}-{base_name}" # First check to see if the server configuration already exists. If so, then return without error exists_check = ds_client.query(kind='cybergym-server') exists_check.add_filter("name", "=", self.server_name) if exists_check.fetch().num_results > 0: cloud_log( LogIDs.MAIN_APP, f"The server {self.server_name} already exists. Skipping configuration.", LOG_LEVELS.ERROR) raise ReferenceError self.build_id = build_id self.custom_image = server_spec.get('image', None) self.build_type = server_spec.get("build_type", None) self.machine_image = server_spec.get("machine_image", None) self.sshkey = server_spec.get("sshkey", None) self.tags = server_spec.get('tags', None) self.machine_type = server_spec.get("machine_type", "n1-standard-1") self.network_routing = server_spec.get("network_routing", None) self.min_cpu_platform = server_spec.get("minCpuPlatform", None) self.add_disk = server_spec.get("add_disk", None) self.options = server_spec.get("options", None) self.snapshot = server_spec.get('snapshot', None) # Add the network configuration self.networks = [] for n in server_spec['nics']: n['external_NAT'] = n[ 'external_NAT'] if 'external_NAT' in n else False nic = { "network": f"{build_id}-{n['network']}", "internal_IP": n['internal_IP'], "subnet": f"{build_id}-{n['network']}-{n['subnet']}", "external_NAT": n['external_NAT'] } # Nested VMs are sometimes used for vulnerable servers. This adds those specified IP addresses as # aliases to the NIC if 'IP_aliases' in n and n['IP_aliases']: alias_ip_ranges = [] for ipaddr in n['IP_aliases']: alias_ip_ranges.append({"ipCidrRange": ipaddr}) nic['aliasIpRanges'] = alias_ip_ranges self.networks.append(nic) # Competitions may have meta_data defined, but compute workouts use assessments. First, check for meta_data # if this is a competition, and then look for startup scripts which have been identified from the assessment self.meta_data = server_spec.get("meta_data", None) if not self.meta_data and startup_scripts and base_name in startup_scripts: self.meta_data = startup_scripts[base_name] self.student_entry = student_entry self.guacamole_startup_script = server_spec.get( 'guacamole_startup_script', None) self.options = options