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
+
+
+
+ Lesson name
+ Solved
+ Number of attempts
+
+
+
+ <% _(lessonStatistics).each(function(lesson) { %>
+ <%= lesson.solved ? '' : ' ' %>
+ <%= 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 @@
+
@@ -112,98 +125,103 @@
-
-
-
+
+
+
+
-
-
-
-
-
- There was an unexpected error. Please try again.
-
-
-
-
-
-
- Show Hints
-
-
-
-
- Lesson overview
-
-
- Reset Lesson
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ There was an unexpected error. Please try
+ again.
-
+
+
+
+
+ Show Hints
+
+
+
+
+ Lesson overview
+
+
+ Reset Lesson
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
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);
+ }
}