diff --git a/Gemfile b/Gemfile index b5090fa0..67c18292 100644 --- a/Gemfile +++ b/Gemfile @@ -50,6 +50,7 @@ end group :test do gem 'brakeman', require: false gem 'bundler-audit', require: false + gem 'byebug' gem 'ffi-hunspell' gem 'rails-controller-testing' gem 'rubocop', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 8d39f896..7967a4f9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,6 +84,7 @@ GEM bundler-audit (0.7.0.1) bundler (>= 1.2.0, < 3) thor (>= 0.18, < 2) + byebug (11.1.3) carrierwave (0.10.0) activemodel (>= 3.2.0) activesupport (>= 3.2.0) @@ -372,6 +373,7 @@ DEPENDENCIES bootstrap4-kaminari-views brakeman bundler-audit + byebug carrierwave-postgresql (< 0.3.0) chartkick coffee-rails diff --git a/app/assets/stylesheets/surveys.scss b/app/assets/stylesheets/surveys.scss new file mode 100644 index 00000000..9bb58850 --- /dev/null +++ b/app/assets/stylesheets/surveys.scss @@ -0,0 +1,56 @@ +// Place all the styles related to the Surveys controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ + +.star-cb-group { + /* remove inline-block whitespace */ + font-size: 0; + /* flip the order so we can use the + and ~ combinators */ + unicode-bidi: bidi-override; + direction: rtl; + /* the hidden clearer */ +} +.star-cb-group * { + font-size: 1rem; +} +.star-cb-group > input { + display: none; +} +.star-cb-group > input + label { + /* only enough room for the star */ + display: inline-block; + overflow: hidden; + text-indent: 9999px; + width: 1em; + white-space: nowrap; + cursor: pointer; +} +.star-cb-group > input + label:before { + display: inline-block; + text-indent: -9999px; + content: "☆"; + color: #888; +} +.star-cb-group > input:checked ~ label:before, .star-cb-group > input + label:hover ~ label:before, .star-cb-group > input + label:hover:before { + content: "★"; + color: rgb(238, 206, 24); + text-shadow: 0 0 1px #333; +} +.star-cb-group > .star-cb-clear + label { + text-indent: -9999px; + width: .5em; + margin-left: -.5em; +} +.star-cb-group > .star-cb-clear + label:before { + width: .5em; +} +.star-cb-group:hover > input + label:before { + content: "☆"; + color: #888; + text-shadow: none; +} +.star-cb-group:hover > input + label:hover ~ label:before, .star-cb-group:hover > input + label:hover:before { + content: "★"; + color: rgb(238, 206, 24); + text-shadow: 0 0 1px #333; +} diff --git a/app/controllers/challenges_controller.rb b/app/controllers/challenges_controller.rb index 49671fcc..58c0823d 100644 --- a/app/controllers/challenges_controller.rb +++ b/app/controllers/challenges_controller.rb @@ -21,10 +21,11 @@ def update @solved_challenge = @flag_found.save_solved_challenge(current_user) @solved_video_url = @flag_found.video_url flash.now[:notice] = I18n.t('flag.accepted') + @survey = Survey.new(submitted_flag_id: @submitted_flag.id) else flash.now[:alert] = wrong_flag_messages.sample + @solvable = @challenge.can_be_solved_by(current_user.team) end - @solvable = @challenge.can_be_solved_by(current_user.team) render :show end @@ -51,7 +52,9 @@ def find_and_log_flag flag = params[:challenge]&.[](:submitted_flag) # Safe navigation on a hash return if flag.nil? - SubmittedFlag.create(user: current_user, challenge: @challenge, text: flag) unless current_user.admin? + unless current_user.admin? + @submitted_flag = SubmittedFlag.create(user: current_user, challenge: @challenge, text: flag) + end @flag_found = @challenge.find_flag(flag) end diff --git a/app/controllers/surveys_controller.rb b/app/controllers/surveys_controller.rb new file mode 100644 index 00000000..f3906dfd --- /dev/null +++ b/app/controllers/surveys_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class SurveysController < ApplicationController + def create + @survey = Survey.new(survey_params) + if(@survey.save) + return redirect_to '/game', notice: I18n.t('surveys.submitted') + else + return redirect_to '/game', notice: @survey.errors.full_messages + end + end + + private + + def survey_params + params.require(:survey).permit(:difficulty, :realism, :interest, :comment, :submitted_flag_id, :team_id) + end +end diff --git a/app/models/submitted_flag.rb b/app/models/submitted_flag.rb index 83c07ef2..f5bc435c 100644 --- a/app/models/submitted_flag.rb +++ b/app/models/submitted_flag.rb @@ -3,6 +3,7 @@ class SubmittedFlag < ApplicationRecord belongs_to :user belongs_to :challenge + has_one :survey, dependent: :destroy validates :text, presence: true diff --git a/app/models/survey.rb b/app/models/survey.rb new file mode 100644 index 00000000..81d4605e --- /dev/null +++ b/app/models/survey.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Survey < ApplicationRecord + belongs_to :submitted_flag, optional: true + belongs_to :team +end diff --git a/app/models/team.rb b/app/models/team.rb index e7fed599..f083275c 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -14,7 +14,7 @@ class Team < ApplicationRecord # Current_score is so we don't have to have any logic to handle # which class we are rendering in the game summary view. attr_accessor :rank, :current_score - + has_many :surveys # This has_many is only applicable to Pentest Challenges has_many :defense_flags, dependent: :destroy has_many :feed_items, dependent: :destroy diff --git a/app/views/challenges/show.html.haml b/app/views/challenges/show.html.haml index a6fe1bf7..87d4a24f 100644 --- a/app/views/challenges/show.html.haml +++ b/app/views/challenges/show.html.haml @@ -1,5 +1,5 @@ -# For PentestGames, @challenge here can actually be a flag object since Pentest challenges belong to teams + challenges and are linked by a flag object. --# +-# - content_for :admin_menu do %a.dropdown-item{:href => admin_edit_url(@challenge)}= t('challenges.admin_edit_challenge', challengename: @challenge.name) @@ -35,7 +35,11 @@ = f.text_field :submitted_flag, :class => "form-control col-sm-10" .control-group = invisible_recaptcha_tags text: 'Submit', :class => "btn btn-primary" - +- if @solved_challenge + %hr/ + = render partial: "surveys/form" + %hr/ + - if @solved_by.length > 0 %table.table.table-bordered.table-striped.table-hover %thead diff --git a/app/views/surveys/_form.html.haml b/app/views/surveys/_form.html.haml new file mode 100644 index 00000000..e284145a --- /dev/null +++ b/app/views/surveys/_form.html.haml @@ -0,0 +1,28 @@ += form_for @survey, html: { class: "well form-inline", style: "margin-bottom:40px;" } do |f| + - if @survey.errors.any? + #error_explanation + %h2= "#{pluralize(@survey.errors.count, "error")} prohibited this survey from being saved:" + %ul + - @survey.errors.full_messages.each do |message| + %li= message + + %h3= t('surveys.new.header') + .rating + .field + = f.label :difficulty + = render partial: "surveys/star_rating", locals: { category: :difficulty, f: f } + .field + = f.label :realism + = render partial: "surveys/star_rating", locals: { category: :realism, f: f } + .field + = f.label :interest + = render partial: "surveys/star_rating", locals: { category: :interest, f: f } + .field + = f.label :comment + = f.text_area :comment + .field + = f.hidden_field :submitted_flag_id + .field + = f.hidden_field :team_id + .field + = f.submit t('surveys.new.submit_btn'), class: "btn btn-primary" diff --git a/app/views/surveys/_star_rating.html.haml b/app/views/surveys/_star_rating.html.haml new file mode 100644 index 00000000..2b8a61e2 --- /dev/null +++ b/app/views/surveys/_star_rating.html.haml @@ -0,0 +1,5 @@ +.rating + %span.star-cb-group + - [5, 4, 3, 2, 1].each do |rating| + = f.radio_button category, rating, { :name => "survey[#{category}]", :id => "#{category}_#{rating}", :value => rating } + %label{:for => "#{category}_#{rating}"} diff --git a/config/initializers/rails_admin.rb b/config/initializers/rails_admin.rb index c68b3e37..09136d54 100644 --- a/config/initializers/rails_admin.rb +++ b/config/initializers/rails_admin.rb @@ -19,7 +19,7 @@ config.actions do # root actions dashboard # mandatory - # collection actions + # collection actions index # mandatory new do except ['Challenge', 'FeedItem', 'SolvedChallenge', 'Flag'] # Block users from adding items from their parent classes instead of their own classes diff --git a/config/locales/en.yml b/config/locales/en.yml index 85d6c784..77edec01 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -435,5 +435,11 @@ en: sign in attempts. unlock_line_2: 'Click the link below to unlock your account:' unlock_account_link: Unlock my account + surveys: + submitted: 'Survey Submitted!' + invalid: 'Invalid survey submitted.' + new: + header: 'Review Solved Challenge' + submit_btn: 'Submit Survey' # Set a default fallback for use if no organization is set on the game. default_organization: 'us' diff --git a/config/routes.rb b/config/routes.rb index fe11ffa9..4640b631 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,6 +58,8 @@ end end + resource :surveys, only: %i[create update] + get '/game/summary' => 'games#summary' get '/game/teams' => 'games#teams' diff --git a/db/migrate/20200710145833_create_surveys.rb b/db/migrate/20200710145833_create_surveys.rb new file mode 100644 index 00000000..ca6847d3 --- /dev/null +++ b/db/migrate/20200710145833_create_surveys.rb @@ -0,0 +1,14 @@ +class CreateSurveys < ActiveRecord::Migration[6.0] + def change + create_table :surveys do |t| + t.integer :difficulty, default: 0, null: false + t.integer :realism, default: 0, null: false + t.integer :interest, default: 0, null: false + t.text :comment, default: "", null: true + t.integer :submitted_flag_id, null: false + t.references :team, foreign_key: true + t.timestamps + end + add_foreign_key "surveys", "submitted_flags" + end +end diff --git a/db/schema.rb b/db/schema.rb index 77cec4b9..c2da4786 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_06_23_173255) do +ActiveRecord::Schema.define(version: 2020_07_10_145833) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -178,6 +178,18 @@ t.index ["flag_id"], name: "index_submitted_flags_on_flag_id" end + create_table "surveys", force: :cascade do |t| + t.integer "difficulty", default: 0, null: false + t.integer "realism", default: 0, null: false + t.integer "interest", default: 0, null: false + t.text "comment", default: "" + t.integer "submitted_flag_id", null: false + t.bigint "team_id" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["team_id"], name: "index_surveys_on_team_id" + end + create_table "teams", id: :serial, force: :cascade do |t| t.string "team_name" t.datetime "created_at" @@ -281,5 +293,7 @@ add_foreign_key "challenges", "games" add_foreign_key "flags", "teams" add_foreign_key "submitted_flags", "flags" + add_foreign_key "surveys", "submitted_flags" + add_foreign_key "surveys", "teams" add_foreign_key "teams", "divisions" end diff --git a/test/controllers/surveys_controller_test.rb b/test/controllers/surveys_controller_test.rb new file mode 100644 index 00000000..e21c1845 --- /dev/null +++ b/test/controllers/surveys_controller_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +class SurveysControllerTest < ActionController::TestCase + + def setup + create(:active_game) + @team1 = create(:team) + @team2 = create(:team) + @standard_challenge = create(:standard_challenge, flag_count: 3) + @pentest_challenge = create(:pentest_challenge_with_flags) + end + + test 'create new survey' do + team_user = create(:user_with_team) + sign_in team_user + submitted_flag = create(:submitted_flag, user: team_user, challenge: @standard_challenge) + assert_difference 'Survey.count', +1 do + post :create, params: { survey: { difficulty: 5, realism: 5, interest: 5, comment: "MyText", submitted_flag_id: submitted_flag.id } } + end + assert_redirected_to game_path + assert_match I18n.t('surveys.submitted'), flash[:notice] + end +end diff --git a/test/factories/surveys.rb b/test/factories/surveys.rb new file mode 100644 index 00000000..6adc3234 --- /dev/null +++ b/test/factories/surveys.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :survey do + difficulty { Faker::Number.between(from:1, to:5) } + realism { Faker::Number.between(from:1, to:5) } + interest { Faker::Number.between(from:1, to:5) } + comment { "MyText" } + submitted_flag_id { 1 } + end +end diff --git a/test/integration/submitted_flags_survey_display_modes_test.rb b/test/integration/submitted_flags_survey_display_modes_test.rb new file mode 100644 index 00000000..8d180618 --- /dev/null +++ b/test/integration/submitted_flags_survey_display_modes_test.rb @@ -0,0 +1,93 @@ +require 'test_helper' + +class SubmittedFlagsSurveyDisplayModesTest < ActionDispatch::IntegrationTest + include TeamsHelper + include Devise::Test::IntegrationHelpers + + def setup + create(:active_game) + @team1 = create(:team) + @team2 = create(:team) + @standard_challenge = create(:standard_challenge, flag_count: 3) + @pentest_challenge = create(:pentest_challenge_with_flags) + end + + test 'solved challenge survey shows for pentest challenge when submitted flag is accepted' do + create(:pentest_solved_challenge, team: @team1, challenge: @pentest_challenge, flag: @team2.defense_flags.first) + sign_in @team1.team_captain + get game_team_challenge_path(@team2, @pentest_challenge) + assert_response :success + if @flag_found + assert_select 'form.well.form-inline' do + assert_select 'h3', I18n.t('surveys.new.header') + assert_select 'div.field', {:count => 3} do + assert_select 'label' + assert_select 'span.star-cb-group' + end + assert_select 'div.field' do + assert_select 'label' + assert_select 'textarea#survey_comment' + end + assert_select 'div.field' do + assert_select 'input#survey_submitted_flag_id' + end + assert_select 'div.field' do + assert_select 'input.btn.btn-primary[type=submit]' + end + end + end + end + + test 'solved challenge survey shows for standard challenge when submitted flag is accepted' do + create(:standard_solved_challenge, challenge: @standard_challenge, team: @team1) + sign_in @team1.team_captain + get game_challenge_path(@standard_challenge) + assert_response :success + if @flag_found + assert_select 'form.well.form-inline' do + assert_select 'h3', I18n.t('surveys.new.header') + assert_select 'div.field', {:count => 3} do + assert_select 'label' + assert_select 'span.star-cb-group' + end + assert_select 'div.field' do + assert_select 'label' + assert_select 'textarea#survey_comment' + end + assert_select 'div.field' do + assert_select 'input#survey_submitted_flag_id' + end + assert_select 'div.field' do + assert_select 'input.btn.btn-primary[type=submit]' + end + end + end + end + + test 'solved challenge survey shows for share challenge when submitted flag is accepted' do + share_chal = create(:share_challenge) + create(:standard_solved_challenge, challenge: share_chal, team: @team1) + sign_in @team1.team_captain + get game_challenge_path(share_chal) + assert_response :success + if @flag_found + assert_select 'form.well.form-inline' do + assert_select 'h3', I18n.t('surveys.new.header') + assert_select 'div.field', {:count => 3} do + assert_select 'label' + assert_select 'span.star-cb-group' + end + assert_select 'div.field' do + assert_select 'label' + assert_select 'textarea#survey_comment' + end + assert_select 'div.field' do + assert_select 'input#survey_submitted_flag_id' + end + assert_select 'div.field' do + assert_select 'input.btn.btn-primary[type=submit]' + end + end + end + end +end diff --git a/test/models/survey_test.rb b/test/models/survey_test.rb new file mode 100644 index 00000000..c4960fc5 --- /dev/null +++ b/test/models/survey_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class SurveyTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/system/surveys_test.rb b/test/system/surveys_test.rb new file mode 100644 index 00000000..a538a01a --- /dev/null +++ b/test/system/surveys_test.rb @@ -0,0 +1,51 @@ +require "application_system_test_case" + +class SurveysTest < ApplicationSystemTestCase + setup do + @survey = surveys(:one) + end + + test "visiting the index" do + visit surveys_url + assert_selector "h1", text: "Surveys" + end + + test "creating a Survey" do + visit surveys_url + click_on "New Survey" + + fill_in "Comment", with: @survey.comment + fill_in "Difficulty", with: @survey.difficulty + fill_in "Interest", with: @survey.interest + fill_in "Realism", with: @survey.realism + fill_in "Submitted flag", with: @survey.submitted_flag_id + click_on "Create Survey" + + assert_text "Survey was successfully created" + click_on "Back" + end + + test "updating a Survey" do + visit surveys_url + click_on "Edit", match: :first + + fill_in "Comment", with: @survey.comment + fill_in "Difficulty", with: @survey.difficulty + fill_in "Interest", with: @survey.interest + fill_in "Realism", with: @survey.realism + fill_in "Submitted flag", with: @survey.submitted_flag_id + click_on "Update Survey" + + assert_text "Survey was successfully updated" + click_on "Back" + end + + test "destroying a Survey" do + visit surveys_url + page.accept_confirm do + click_on "Destroy", match: :first + end + + assert_text "Survey was successfully destroyed" + end +end