From e2cb9ceae008c2e3eccc70c203ec4a742ccdbd08 Mon Sep 17 00:00:00 2001 From: Nanne Baars Date: Sat, 31 Dec 2016 18:27:20 +0100 Subject: [PATCH] #277 Re-institute admin functionality for WebGoat 8 - Report card functionality is back --- .../webgoat/service/ReportCardService.java | 111 +++++++++++ .../org/owasp/webgoat/session/Course.java | 9 + .../owasp/webgoat/session/UserTracker.java | 21 +++ .../src/main/resources/application.properties | 2 +- .../js/goatApp/controller/LessonController.js | 7 +- .../js/goatApp/controller/MenuController.js | 15 +- .../js/goatApp/model/ReportCardModel.js | 8 + .../js/goatApp/templates/report_card.html | 47 +++++ .../static/js/goatApp/view/GoatRouter.js | 100 ++++++---- .../static/js/goatApp/view/ReportCardView.js | 21 +++ .../src/main/resources/static/js/main.js | 6 +- .../main/resources/templates/main_new.html | 174 ++++++++++-------- .../service/ReportCardServiceTest.java | 59 ++++++ .../webgoat/session/UserTrackerTest.java | 12 ++ 14 files changed, 457 insertions(+), 135 deletions(-) create mode 100644 webgoat-container/src/main/java/org/owasp/webgoat/service/ReportCardService.java create mode 100644 webgoat-container/src/main/resources/static/js/goatApp/model/ReportCardModel.js create mode 100644 webgoat-container/src/main/resources/static/js/goatApp/templates/report_card.html create mode 100644 webgoat-container/src/main/resources/static/js/goatApp/view/ReportCardView.js create mode 100644 webgoat-container/src/test/java/org/owasp/webgoat/service/ReportCardServiceTest.java diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/service/ReportCardService.java b/webgoat-container/src/main/java/org/owasp/webgoat/service/ReportCardService.java new file mode 100644 index 000000000..1fc17a5ba --- /dev/null +++ b/webgoat-container/src/main/java/org/owasp/webgoat/service/ReportCardService.java @@ -0,0 +1,111 @@ +/** + * ************************************************************************************************* + *

+ *

+ * This file is part of WebGoat, an Open Web Application Security Project + * utility. For details, please see http://www.owasp.org/ + *

+ * Copyright (c) 2002 - 20014 Bruce Mayhew + *

+ * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + *

+ * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + *

+ * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 59 Temple + * Place - Suite 330, Boston, MA 02111-1307, USA. + *

+ * Getting Source ============== + *

+ * Source for this application is maintained at + * https://github.com/WebGoat/WebGoat, a repository for free software projects. + */ +package org.owasp.webgoat.service; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import lombok.Getter; +import lombok.Setter; +import lombok.Singular; +import org.apache.catalina.User; +import org.owasp.webgoat.lessons.AbstractLesson; +import org.owasp.webgoat.session.Course; +import org.owasp.webgoat.session.LessonTracker; +import org.owasp.webgoat.session.UserTracker; +import org.owasp.webgoat.session.WebSession; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + *

ReportCardService

+ * + * @author nbaars + * @version $Id: $Id + */ +@Controller +public class ReportCardService { + + private final UserTracker userTracker; + private final Course course; + + public ReportCardService(UserTracker userTracker, Course course) { + this.userTracker = userTracker; + this.course = course; + } + + /** + * Endpoint which generates the report card for the current use to show the stats on the solved lessons + */ + @GetMapping(path = "/service/reportcard.mvc", produces = "application/json") + @ResponseBody + public ReportCard reportCard() { + List lessons = course.getLessons(); + ReportCard reportCard = new ReportCard(); + reportCard.setTotalNumberOfLessons(course.getTotalOfLessons()); + reportCard.setTotalNumberOfAssignments(course.getTotalOfAssignments()); + reportCard.setNumberOfAssignmentsSolved(userTracker.numberOfAssignmentsSolved()); + reportCard.setNumberOfLessonsSolved(userTracker.numberOfLessonsSolved()); + for (AbstractLesson lesson : lessons) { + LessonTracker lessonTracker = userTracker.getLessonTracker(lesson); + LessonStatistics lessonStatistics = new LessonStatistics(); + lessonStatistics.setName(lesson.getTitle()); + lessonStatistics.setNumberOfAttempts(lessonTracker.getNumberOfAttempts()); + lessonStatistics.setSolved(lessonTracker.isLessonSolved()); + reportCard.lessonStatistics.add(lessonStatistics); + } + return reportCard; + } + + @Getter + @Setter + private class ReportCard { + + private int totalNumberOfLessons; + private int totalNumberOfAssignments; + private int solvedLessons; + private int numberOfAssignmentsSolved; + private int numberOfLessonsSolved; + private List lessonStatistics = Lists.newArrayList(); + } + + @Setter + @Getter + private class LessonStatistics { + private String name; + private boolean solved; + private int numberOfAttempts; + } +} diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/session/Course.java b/webgoat-container/src/main/java/org/owasp/webgoat/session/Course.java index 73f263114..9c1de57a6 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/session/Course.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/session/Course.java @@ -89,5 +89,14 @@ public class Course { this.lessons = lessons; } + public int getTotalOfLessons() { + return this.lessons.size(); + } + + public int getTotalOfAssignments() { + final int[] total = {0}; + this.lessons.stream().forEach(l -> total[0] = total[0] + l.getAssignments().size()); + return total[0]; + } } diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/session/UserTracker.java b/webgoat-container/src/main/java/org/owasp/webgoat/session/UserTracker.java index b05512113..26dde2ca9 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/session/UserTracker.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/session/UserTracker.java @@ -4,12 +4,14 @@ package org.owasp.webgoat.session; import com.google.common.collect.Maps; import lombok.SneakyThrows; import org.owasp.webgoat.lessons.AbstractLesson; +import org.owasp.webgoat.lessons.Assignment; import org.springframework.util.FileCopyUtils; import org.springframework.util.SerializationUtils; import java.io.File; import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; /** @@ -104,4 +106,23 @@ public class UserTracker { getLessonTracker(al).reset(); save(); } + + public int numberOfLessonsSolved() { + int numberOfLessonsSolved = 0; + for(LessonTracker lessonTracker : storage.values()) { + if (lessonTracker.isLessonSolved()) { + numberOfLessonsSolved = numberOfLessonsSolved + 1; + } + } + return numberOfLessonsSolved; + } + + public int numberOfAssignmentsSolved() { + int numberOfAssignmentsSolved = 0; + for (LessonTracker lessonTracker : storage.values()) { + Map lessonOverview = lessonTracker.getLessonOverview(); + numberOfAssignmentsSolved = lessonOverview.values().stream().filter(b -> b).collect(Collectors.counting()).intValue(); + } + return numberOfAssignmentsSolved; + } } diff --git a/webgoat-container/src/main/resources/application.properties b/webgoat-container/src/main/resources/application.properties index 6eeb2f6bd..553aef6bf 100644 --- a/webgoat-container/src/main/resources/application.properties +++ b/webgoat-container/src/main/resources/application.properties @@ -16,7 +16,7 @@ spring.devtools.restart.enabled=false spring.resources.cache-period=0 -webgoat.tracker.overwrite=true +webgoat.tracker.overwrite=false webgoat.user.directory=${user.home}/.webgoat/ webgoat.build.version=@project.version@ webgoat.build.number=@build.number@ diff --git a/webgoat-container/src/main/resources/static/js/goatApp/controller/LessonController.js b/webgoat-container/src/main/resources/static/js/goatApp/controller/LessonController.js index 1f6f1f935..125b3cc15 100644 --- a/webgoat-container/src/main/resources/static/js/goatApp/controller/LessonController.js +++ b/webgoat-container/src/main/resources/static/js/goatApp/controller/LessonController.js @@ -55,9 +55,9 @@ define(['jquery', this.lessonOverviewModel = new LessonOverviewModel(); this.lessonOverview = new LessonOverviewView(this.lessonOverviewModel); this.lessonContentView = options.lessonContentView; + this.titleView = options.titleView; this.developerControlsView = new DeveloperControlsView(); - _.extend(Controller.prototype,Backbone.Events); this.start = function() { @@ -67,12 +67,13 @@ define(['jquery', }; this.loadLesson = function(name,pageNum) { + if (this.name === name) { - this.lessonContentView.navToPage(pageNum) + this.lessonContentView.navToPage(pageNum); + this.titleView.render(this.lessonInfoModel.get('lessonTitle')); return; } - this.titleView = new TitleView(); this.helpsLoaded = {}; if (typeof(name) === 'undefined' || name === null) { //TODO: implement lesson not found or return to welcome page? diff --git a/webgoat-container/src/main/resources/static/js/goatApp/controller/MenuController.js b/webgoat-container/src/main/resources/static/js/goatApp/controller/MenuController.js index 933a0a05c..278ec0426 100644 --- a/webgoat-container/src/main/resources/static/js/goatApp/controller/MenuController.js +++ b/webgoat-container/src/main/resources/static/js/goatApp/controller/MenuController.js @@ -2,7 +2,7 @@ define(['jquery', 'underscore', 'backbone', 'goatApp/view/MenuView' - ], + ], function($, _, Backbone, @@ -11,22 +11,9 @@ define(['jquery', _.extend(Controller.prototype,Backbone.Events); options = options || {}; this.menuView = options.menuView; - this.titleView = options.titleView; - - // this.initMenu = function() { - // this.listenTo(this.menuView,'lesson:click',this.renderTitle); - // } - this.updateMenu = function(){ this.menuView.updateMenu(); - }, - - //TODO: move title rendering into lessonContent/View pipeline once data can support it - this.renderTitle = function(title) { - this.titleView.render(title); } - - }; return Controller; diff --git a/webgoat-container/src/main/resources/static/js/goatApp/model/ReportCardModel.js b/webgoat-container/src/main/resources/static/js/goatApp/model/ReportCardModel.js new file mode 100644 index 000000000..409b42d82 --- /dev/null +++ b/webgoat-container/src/main/resources/static/js/goatApp/model/ReportCardModel.js @@ -0,0 +1,8 @@ +define([ + 'backbone'], + function( + Backbone) { + return Backbone.Model.extend({ + url: 'service/reportcard.mvc' + }); +}); \ No newline at end of file diff --git a/webgoat-container/src/main/resources/static/js/goatApp/templates/report_card.html b/webgoat-container/src/main/resources/static/js/goatApp/templates/report_card.html new file mode 100644 index 000000000..94d414c67 --- /dev/null +++ b/webgoat-container/src/main/resources/static/js/goatApp/templates/report_card.html @@ -0,0 +1,47 @@ +
+
Overview
+ + + + + + + + + + + + + + + + + + + + +
Total number of lessons<%= totalNumberOfLessons %>
Total number of lessons solved<%= numberOfLessonsSolved %>
Total number of assignments<%= totalNumberOfAssignments %>
Total number of assignments solved<%= numberOfAssignmentsSolved %>
+
+ + +
+
Lesson overview
+ + + + + + + + + + <% _(lessonStatistics).each(function(lesson) { %> + <%= lesson.solved ? '' : '' %> + + + + + <% }) %> + +
Lesson nameSolvedNumber of attempts
<%= lesson.name %><%= lesson.solved %><%= lesson.numberOfAttempts %>
+
\ No newline at end of file diff --git a/webgoat-container/src/main/resources/static/js/goatApp/view/GoatRouter.js b/webgoat-container/src/main/resources/static/js/goatApp/view/GoatRouter.js index 2039cf1e2..b1a219667 100644 --- a/webgoat-container/src/main/resources/static/js/goatApp/view/GoatRouter.js +++ b/webgoat-container/src/main/resources/static/js/goatApp/view/GoatRouter.js @@ -5,37 +5,56 @@ define(['jquery', 'goatApp/controller/MenuController', 'goatApp/view/LessonContentView', 'goatApp/view/MenuView', - 'goatApp/view/DeveloperControlsView' - ], function ($, - _, - Backbone, - LessonController, - MenuController, - LessonContentView, - MenuView, - DeveloperControlsView) { - + 'goatApp/view/DeveloperControlsView', + 'goatApp/view/TitleView' +], function ($, + _, + Backbone, + LessonController, + MenuController, + LessonContentView, + MenuView, + DeveloperControlsView, + TitleView) { + var lessonContentView = new LessonContentView(); var menuView = new MenuView(); var developerControlsView = new DeveloperControlsView(); + var titleView = new TitleView(); + + function getContentElement() { + return $('#main-content'); + }; + + function render(view) { + $('div.pages').hide(); + //TODO this works for now because we only have one page we should rewrite this a bit + if (view != null) { + $('#report-card-page').show(); + } else { + $('#lesson-title').show(); + $('#lesson-page').show(); + } + }; var GoatAppRouter = Backbone.Router.extend({ routes: { - 'welcome':'welcomeRoute', - 'lesson/:name':'lessonRoute', - 'lesson/:name/:pageNum':'lessonPageRoute', - 'test/:param':'testRoute' + 'welcome': 'welcomeRoute', + 'lesson/:name': 'lessonRoute', + 'lesson/:name/:pageNum': 'lessonPageRoute', + 'test/:param': 'testRoute', + 'reportCard': 'reportCard' }, lessonController: new LessonController({ - lessonContentView: lessonContentView + lessonContentView: lessonContentView, + titleView: titleView }), menuController: new MenuController({ menuView: menuView }), - setUpCustomJS: function () { webgoat.customjs.jquery = $; //passing jquery into custom js scope ... still klunky, but works for now @@ -45,19 +64,19 @@ define(['jquery', console.log(arguments.callee); // webgoat.customjs.jquery.ajax({ - method:"POST", - url:"/WebGoat/CrossSiteScripting/dom-xss", - data:{param1:42,param2:24}, - headers:{ - "webgoat-requested-by":"dom-xss-vuln" - }, - contentType:'application/x-www-form-urlencoded; charset=UTF-8' + method: "POST", + url: "/WebGoat/CrossSiteScripting/dom-xss", + data: {param1: 42, param2: 24}, + headers: { + "webgoat-requested-by": "dom-xss-vuln" + }, + contentType: 'application/x-www-form-urlencoded; charset=UTF-8' }); } }, - init:function() { - goatRouter = new GoatAppRouter(); + init: function () { + goatRouter = new GoatAppRouter(); this.lessonController.start(); // this.menuController.initMenu(); webgoat = {}; @@ -65,39 +84,48 @@ define(['jquery', this.setUpCustomJS(); - - goatRouter.on('route:lessonRoute', function(name) { - this.lessonController.loadLesson(name,0); + goatRouter.on('route:lessonRoute', function (name) { + render(); + this.lessonController.loadLesson(name, 0); //TODO - update menu code from below this.menuController.updateMenu(name); }); - goatRouter.on('route:lessonPageRoute', function(name,pageNum) { + goatRouter.on('route:lessonPageRoute', function (name, pageNum) { + render(); pageNum = (_.isNumber(parseInt(pageNum))) ? parseInt(pageNum) : 0; - this.lessonController.loadLesson(name,pageNum); + this.lessonController.loadLesson(name, pageNum); //TODO - update menu code from below this.menuController.updateMenu(name); }); - goatRouter.on('route:welcomeRoute', function() { + goatRouter.on('route:welcomeRoute', function () { + render(); this.lessonController.loadWelcome(); }); - goatRouter.on('route:testRoute', function(param) { + goatRouter.on('route:testRoute', function (param) { + render(); this.lessonController.testHandler(param); }); - goatRouter.on("route", function(route, params) {}); + goatRouter.on("route", function (route, params) { + }); Backbone.history.start(); - this.listenTo(this.lessonController, 'menu:reload',this.reloadMenu) + this.listenTo(this.lessonController, 'menu:reload', this.reloadMenu) }, reloadMenu: function (curLesson) { this.menuController.updateMenu(); - } - + }, + reportCard : function () { + require(['goatApp/view/ReportCardView'], function (ReportCardView) { + titleView.render('Report card'); + render(new ReportCardView()); + }); + }, }); return GoatAppRouter; diff --git a/webgoat-container/src/main/resources/static/js/goatApp/view/ReportCardView.js b/webgoat-container/src/main/resources/static/js/goatApp/view/ReportCardView.js new file mode 100644 index 000000000..9fd558790 --- /dev/null +++ b/webgoat-container/src/main/resources/static/js/goatApp/view/ReportCardView.js @@ -0,0 +1,21 @@ +define(['jquery', 'backbone', 'underscore', 'goatApp/model/ReportCardModel', 'text!templates/report_card.html'], + function ($, Backbone, _, ReportCardModel, ReportCardTemplate) { + return Backbone.View.extend({ + el: '#report-card-page', + template: ReportCardTemplate, + + initialize: function () { + var _this = this; + this.model = new ReportCardModel(); + this.model.fetch().then(function() { + _this.render(); + }); + }, + + render: function () { + var t = _.template(this.template); + this.$el.html(t(this.model.toJSON())); + return this; + } + }); + }); \ No newline at end of file diff --git a/webgoat-container/src/main/resources/static/js/main.js b/webgoat-container/src/main/resources/static/js/main.js index 64f4ea68c..d1507f2a4 100644 --- a/webgoat-container/src/main/resources/static/js/main.js +++ b/webgoat-container/src/main/resources/static/js/main.js @@ -16,9 +16,9 @@ require.config({ jquery: 'libs/jquery-1.10.2.min', underscore: 'libs/underscore-min', backbone: 'libs/backbone-min', - templates: 'goatApp/templates' - } -, + text: 'libs/text', + templates: 'goatApp/templates', + }, shim: { underscore: { exports: "_" diff --git a/webgoat-container/src/main/resources/templates/main_new.html b/webgoat-container/src/main/resources/templates/main_new.html index 53cf9bb61..3cd599165 100644 --- a/webgoat-container/src/main/resources/templates/main_new.html +++ b/webgoat-container/src/main/resources/templates/main_new.html @@ -90,6 +90,19 @@ +
+ + + + + + + + +
- - - - - - - - -
- -
-
-
- - -
- + +
+
+
+
+ There was an unexpected error. Please try + again.
-
+
+ + + + + + + +
+ +
+ +
+
+
+ + +
+ +
+
+
+ +
+
+
+
+
+ +
-
-
-
+
+
+
+
+
+
+

Lesson Plan

-
+
+
+ +
+
+
+
+
+
+

Lesson Solution

+
+
+
+
+
+
+
+
+

Lesson Source Code

+ +
+
+
-
- - - -
-
-

Lesson Plan

- -
-
- -
-
-
-
-
-
-

Lesson Solution

- -
-
-
-
-
-
-
-
-

Lesson Source Code

- -
-
-
-
-
-
+ diff --git a/webgoat-container/src/test/java/org/owasp/webgoat/service/ReportCardServiceTest.java b/webgoat-container/src/test/java/org/owasp/webgoat/service/ReportCardServiceTest.java new file mode 100644 index 000000000..bd3685d86 --- /dev/null +++ b/webgoat-container/src/test/java/org/owasp/webgoat/service/ReportCardServiceTest.java @@ -0,0 +1,59 @@ +package org.owasp.webgoat.service; + +import com.beust.jcommander.internal.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.owasp.webgoat.lessons.AbstractLesson; +import org.owasp.webgoat.session.Course; +import org.owasp.webgoat.session.LessonTracker; +import org.owasp.webgoat.session.UserTracker; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.hamcrest.CoreMatchers.is; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; + +@RunWith(MockitoJUnitRunner.class) +public class ReportCardServiceTest { + + private MockMvc mockMvc; + @Mock + private Course course; + @Mock + private UserTracker userTracker; + @Mock + private AbstractLesson lesson; + @Mock + private LessonTracker lessonTracker; + + @Before + public void setup() { + this.mockMvc = standaloneSetup(new ReportCardService(userTracker, course)).build(); + } + + @Test + @WithMockUser(username = "guest", password = "guest") + public void withLessons() throws Exception { + when(lesson.getTitle()).thenReturn("Test"); + when(course.getTotalOfLessons()).thenReturn(1); + when(course.getTotalOfAssignments()).thenReturn(10); + when(course.getLessons()).thenReturn(Lists.newArrayList(lesson)); + when(userTracker.getLessonTracker(any())).thenReturn(lessonTracker); + mockMvc.perform(MockMvcRequestBuilders.get("/service/reportcard.mvc")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.totalNumberOfLessons", is(1))) + .andExpect(jsonPath("$.solvedLessons", is(0))) + .andExpect(jsonPath("$.numberOfAssignmentsSolved", is(0))) + .andExpect(jsonPath("$.totalNumberOfAssignments", is(10))) + .andExpect(jsonPath("$.lessonStatistics[0].name", is("Test"))) + .andExpect(jsonPath("$.numberOfAssignmentsSolved", is(0))); + } +} diff --git a/webgoat-container/src/test/java/org/owasp/webgoat/session/UserTrackerTest.java b/webgoat-container/src/test/java/org/owasp/webgoat/session/UserTrackerTest.java index 302cab5de..b2643274a 100644 --- a/webgoat-container/src/test/java/org/owasp/webgoat/session/UserTrackerTest.java +++ b/webgoat-container/src/test/java/org/owasp/webgoat/session/UserTrackerTest.java @@ -10,6 +10,7 @@ import java.io.File; import java.io.IOException; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -88,4 +89,15 @@ public class UserTrackerTest { userTracker.reset(lesson); assertThat(userTracker.getLessonTracker(lesson).isLessonSolved()).isFalse(); } + + @Test + public void totalAssignmentsSolved() { + UserTracker userTracker = new UserTracker(home.getParent(), "test", false); + AbstractLesson lesson = mock(AbstractLesson.class); + when(lesson.getAssignments()).thenReturn(Lists.newArrayList(new Assignment("assignment", "assignment"))); + userTracker.assignmentSolved(lesson, "assignment"); + + assertThat(userTracker.numberOfAssignmentsSolved()).isEqualTo(1); + assertThat(userTracker.numberOfLessonsSolved()).isEqualTo(1); + } }