diff --git a/grader.exs b/grader.exs
deleted file mode 100644
index be09905..0000000
--- a/grader.exs
+++ /dev/null
@@ -1,82 +0,0 @@
-# Usage: mix run grader.exs
-
-defmodule Grader.Client do
- use GenServer
-
- alias Livebook.{Session, LiveMarkdown}
-
- def run_and_save(notebook_path) do
- {:ok, pid} = GenServer.start(__MODULE__, {notebook_path})
-
- ref = Process.monitor(pid)
-
- receive do
- {:DOWN, ^ref, :process, _pid, _reason} -> :ok
- end
- end
-
- @impl true
- def init({notebook_path}) do
- IO.puts("Evaluating: #{notebook_path}")
- {notebook, _messages} =
- notebook_path
- |> File.read!()
- |> LiveMarkdown.notebook_from_livemd()
-
- {:ok, session} = Livebook.Sessions.create_session(notebook: notebook)
-
- {data, _client_id} = Session.register_client(session.pid, self(), Livebook.Users.User.new())
-
- Session.subscribe(session.id)
- Session.queue_full_evaluation(session.pid, [])
-
- {:ok, %{notebook_path: notebook_path, session: session, data: data, evaluated_count: 0}}
- end
-
- @impl true
- def handle_info({:operation, operation}, state) do
- data =
- case Session.Data.apply_operation(state.data, operation) do
- {:ok, data, _actions} -> data
- :error -> state.data
- end
-
- {evaluated_count, evaluable_count} = evaluation_progress(data)
-
- if evaluated_count != state.evaluated_count do
- IO.puts("Evaluated cell: #{evaluated_count}/#{evaluable_count}")
- end
-
- if evaluated_count == evaluable_count do
- content = LiveMarkdown.notebook_to_livemd(data.notebook, include_outputs: true)
- File.write!(state.notebook_path, content)
- {:stop, :shutdown, state}
- else
- {:noreply, %{state | data: data, evaluated_count: evaluated_count}}
- end
- end
-
- def handle_info(_message, state), do: {:noreply, state}
-
- defp evaluation_progress(data) do
- evaluable = Livebook.Notebook.evaluable_cells_with_section(data.notebook)
-
- evaluated_count =
- Enum.count(evaluable, fn {cell, _} ->
- match?(%{validity: :evaluated, status: :ready}, data.cell_infos[cell.id].eval)
- end)
-
- {evaluated_count, length(evaluable)}
- end
-end
-
-defmodule Grader.App do
- def main() do
- Path.wildcard("./modules/*.livemd")
- |> Enum.each(fn module -> Grader.Client.run_and_save(module) end)
-
- IO.puts("Saved module outputs")
- end
-end
-
-Grader.App.main()
diff --git a/grading_server/.formatter.exs b/grading_server/.formatter.exs
new file mode 100644
index 0000000..875b9bc
--- /dev/null
+++ b/grading_server/.formatter.exs
@@ -0,0 +1,5 @@
+[
+ import_deps: [:phoenix],
+ inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"],
+ subdirectories: []
+]
diff --git a/grading_server/.gitignore b/grading_server/.gitignore
new file mode 100644
index 0000000..d135772
--- /dev/null
+++ b/grading_server/.gitignore
@@ -0,0 +1,36 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Ignore package tarball (built via "mix hex.build").
+testy-*.tar
+
+# Ignore assets that are produced by build tools.
+/priv/static/assets/
+
+# Ignore digested assets cache.
+/priv/static/cache_manifest.json
+
+# In case you use Node.js/npm, you want to ignore these.
+npm-debug.log
+/assets/node_modules/
+
+!.gitignore
+!.sobelow-conf
diff --git a/grading_server/.sobelow-conf b/grading_server/.sobelow-conf
new file mode 100644
index 0000000..a393b0b
--- /dev/null
+++ b/grading_server/.sobelow-conf
@@ -0,0 +1,12 @@
+[
+ verbose: true,
+ private: false,
+ skip: false,
+ router: "",
+ exit: "false",
+ format: "txt",
+ out: "",
+ threshold: "medium",
+ ignore: ["Config.HTTPS"],
+ ignore_files: [""]
+]
diff --git a/grading_server/README.md b/grading_server/README.md
new file mode 100644
index 0000000..765ce6c
--- /dev/null
+++ b/grading_server/README.md
@@ -0,0 +1,18 @@
+# ESCT - Grading Server
+
+To start your Phoenix server:
+
+ * Install dependencies with `mix deps.get`
+ * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
+
+Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
+
+Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
+
+## Deploying your own version
+
+This only applies if you've created your own Grading Server and wish to lock down the API to your internal users.
+
+1. Uncomment the `plug SimpleTokenAuthentication` line in the `router.ex` file
+2. Change the value of the config for `:simple_token_authentication` in `config/config.exs`
+3. Update the `priv/answers.yml` to reference actual answers.
diff --git a/grading_server/config/config.exs b/grading_server/config/config.exs
new file mode 100644
index 0000000..5401882
--- /dev/null
+++ b/grading_server/config/config.exs
@@ -0,0 +1,34 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Config module.
+#
+# This configuration file is loaded before any dependency and
+# is restricted to this project.
+
+# General application configuration
+import Config
+
+config :grading_server,
+ answer_store_file: "answers.yml"
+
+# Configures the endpoint
+config :grading_server, GradingServerWeb.Endpoint,
+ url: [host: "localhost"],
+ render_errors: [view: GradingServerWeb.ErrorView, accepts: ~w(json), layout: false],
+ pubsub_server: GradingServer.PubSub,
+ live_view: [signing_salt: "HVVkYc2t"]
+
+# Configures Elixir's Logger
+config :logger, :console,
+ format: "$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
+config :simple_token_authentication,
+ # CHANGE ME IF IN USE
+ token: "my-token"
+
+# Import environment specific config. This must remain at the bottom
+# of this file so it overrides the configuration defined above.
+import_config "#{config_env()}.exs"
diff --git a/grading_server/config/dev.exs b/grading_server/config/dev.exs
new file mode 100644
index 0000000..c450933
--- /dev/null
+++ b/grading_server/config/dev.exs
@@ -0,0 +1,51 @@
+import Config
+
+# For development, we disable any cache and enable
+# debugging and code reloading.
+#
+# The watchers configuration can be used to run external
+# watchers to your application. For example, we use it
+# with esbuild to bundle .js and .css sources.
+config :grading_server, GradingServerWeb.Endpoint,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {127, 0, 0, 1}, port: 4000],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "3v9wINF+nNWThqV/ypHIqzb73TzIRmimofVeKeNcbKqM9WWV5qNC5h4ofMLuQxu2",
+ watchers: []
+
+# ## SSL Support
+#
+# In order to use HTTPS in development, a self-signed
+# certificate can be generated by running the following
+# Mix task:
+#
+# mix phx.gen.cert
+#
+# Note that this task requires Erlang/OTP 20 or later.
+# Run `mix help phx.gen.cert` for more information.
+#
+# The `http:` config above can be replaced with:
+#
+# https: [
+# port: 4001,
+# cipher_suite: :strong,
+# keyfile: "priv/cert/selfsigned_key.pem",
+# certfile: "priv/cert/selfsigned.pem"
+# ],
+#
+# If desired, both `http:` and `https:` keys can be
+# configured to run both http and https servers on
+# different ports.
+
+# Do not include metadata nor timestamps in development logs
+config :logger, :console, format: "[$level] $message\n"
+
+# Set a higher stacktrace during development. Avoid configuring such
+# in production as building large stacktraces may be expensive.
+config :phoenix, :stacktrace_depth, 20
+
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
diff --git a/grading_server/config/prod.exs b/grading_server/config/prod.exs
new file mode 100644
index 0000000..5ff2deb
--- /dev/null
+++ b/grading_server/config/prod.exs
@@ -0,0 +1,49 @@
+import Config
+
+# For production, don't forget to configure the url host
+# to something meaningful, Phoenix uses this information
+# when generating URLs.
+#
+# Note we also include the path to a cache manifest
+# containing the digested version of static files. This
+# manifest is generated by the `mix phx.digest` task,
+# which you should run after static files are built and
+# before starting your production server.
+config :grading_server, GradingServerWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Do not print debug messages in production
+config :logger, level: :info
+
+# ## SSL Support
+#
+# To get SSL working, you will need to add the `https` key
+# to the previous section and set your `:url` port to 443:
+#
+# config :grading_server, GradingServerWeb.Endpoint,
+# ...,
+# url: [host: "example.com", port: 443],
+# https: [
+# ...,
+# port: 443,
+# cipher_suite: :strong,
+# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
+# ]
+#
+# The `cipher_suite` is set to `:strong` to support only the
+# latest and more secure SSL ciphers. This means old browsers
+# and clients may not be supported. You can set it to
+# `:compatible` for wider support.
+#
+# `:keyfile` and `:certfile` expect an absolute path to the key
+# and cert in disk or a relative path inside priv, for example
+# "priv/ssl/server.key". For all supported SSL configuration
+# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+#
+# We also recommend setting `force_ssl` in your endpoint, ensuring
+# no data is ever sent via http, always redirecting to https:
+#
+# config :grading_server, GradingServerWeb.Endpoint,
+# force_ssl: [hsts: true]
+#
+# Check `Plug.SSL` for all available options in `force_ssl`.
diff --git a/grading_server/config/runtime.exs b/grading_server/config/runtime.exs
new file mode 100644
index 0000000..2c95332
--- /dev/null
+++ b/grading_server/config/runtime.exs
@@ -0,0 +1,50 @@
+import Config
+
+# config/runtime.exs is executed for all environments, including
+# during releases. It is executed after compilation and before the
+# system starts, so it is typically used to load production configuration
+# and secrets from environment variables or elsewhere. Do not define
+# any compile-time configuration in here, as it won't be applied.
+# The block below contains prod specific runtime configuration.
+
+# ## Using releases
+#
+# If you use `mix release`, you need to explicitly enable the server
+# by passing the PHX_SERVER=true when you start it:
+#
+# PHX_SERVER=true bin/grading_server start
+#
+# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
+# script that automatically sets the env var above.
+if System.get_env("PHX_SERVER") do
+ config :grading_server, GradingServerWeb.Endpoint, server: true
+end
+
+if config_env() == :prod do
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("PHX_HOST") || "example.com"
+ port = String.to_integer(System.get_env("PORT") || "4000")
+
+ config :grading_server, GradingServerWeb.Endpoint,
+ url: [host: host, port: 443, scheme: "https"],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: port
+ ],
+ secret_key_base: secret_key_base
+end
diff --git a/grading_server/config/test.exs b/grading_server/config/test.exs
new file mode 100644
index 0000000..e57c8e6
--- /dev/null
+++ b/grading_server/config/test.exs
@@ -0,0 +1,14 @@
+import Config
+
+# We don't run a server during test. If one is required,
+# you can enable the server option below.
+config :grading_server, GradingServerWeb.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "d//8uGEq9XnUKtzlFN5q5ia38hfd3HI38K6wWjXBEDsIJCaZMTPfX0z3biIX2qVQ",
+ server: false
+
+# Print only warnings and errors during test
+config :logger, level: :warn
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
diff --git a/grading_server/docker-compose.yaml b/grading_server/docker-compose.yaml
new file mode 100644
index 0000000..7d89975
--- /dev/null
+++ b/grading_server/docker-compose.yaml
@@ -0,0 +1,15 @@
+version: "3.8"
+services:
+ db:
+ image: postgres:14.4
+ restart: always
+ environment:
+ - POSTGRES_PASSWORD=postgres
+ - POSTGRES_USER=postgres
+ ports:
+ - 5432:5432
+ volumes:
+ - db:/var/lib/postgresql/data
+volumes:
+ db:
+ driver: local
\ No newline at end of file
diff --git a/grading_server/lib/grading_server.ex b/grading_server/lib/grading_server.ex
new file mode 100644
index 0000000..c83d629
--- /dev/null
+++ b/grading_server/lib/grading_server.ex
@@ -0,0 +1,9 @@
+defmodule GradingServer do
+ @moduledoc """
+ GradingServer keeps the contexts that define your domain
+ and business logic.
+
+ Contexts are also responsible for managing your data, regardless
+ if it comes from the database, an external API or others.
+ """
+end
diff --git a/grading_server/lib/grading_server/answer.ex b/grading_server/lib/grading_server/answer.ex
new file mode 100644
index 0000000..e708102
--- /dev/null
+++ b/grading_server/lib/grading_server/answer.ex
@@ -0,0 +1,15 @@
+defmodule GradingServer.Answer do
+ @moduledoc """
+ This is a representation of an answer.
+ """
+
+ @enforce_keys [:question_id, :module_id, :answer, :help_text]
+ defstruct [:question_id, :module_id, :answer, :help_text]
+
+ @type t :: %__MODULE__{
+ module_id: integer(),
+ question_id: integer(),
+ answer: String.t(),
+ help_text: String.t()
+ }
+end
diff --git a/grading_server/lib/grading_server/answer_store.ex b/grading_server/lib/grading_server/answer_store.ex
new file mode 100644
index 0000000..2551463
--- /dev/null
+++ b/grading_server/lib/grading_server/answer_store.ex
@@ -0,0 +1,78 @@
+defmodule GradingServer.AnswerStore do
+ @moduledoc """
+ This is a representatino of the `priv/answers.yml` file. It utilizes a write back caching mechanism.
+ """
+
+ use GenServer
+
+ alias GradingServer.Answer
+
+ @table :answer_store
+
+ def start_link(opts) do
+ GenServer.start_link(__MODULE__, opts, name: __MODULE__)
+ end
+
+ @impl true
+ def init(state) do
+ :ets.new(@table, [:set, :named_table])
+
+ Enum.each(load_answers(), fn answer ->
+ :ets.insert(@table, {key(answer.module_id, answer.question_id), answer})
+ end)
+
+ {:ok, state}
+ end
+
+ defp key(module_id, question_id), do: "module_#{module_id}-#{question_id}}"
+
+ @impl true
+ def handle_call({:fetch, module_id, question_id}, _from, _) do
+ item = :ets.lookup(@table, key(module_id, question_id))
+
+ item =
+ if Enum.empty?(item) do
+ nil
+ else
+ [{_id, item}] = item
+ item
+ end
+
+ {:reply, item, nil}
+ end
+
+ @doc """
+ Fetches the given answer from the store.
+ """
+ @spec get_answer(integer(), integer()) :: Answer.t() | nil
+ def get_answer(module_id, question_id) do
+ GenServer.call(__MODULE__, {:fetch, module_id, question_id})
+ end
+
+ # load a file from the priv folder
+ @spec load_answers() :: [Answer.t()]
+ def load_answers() do
+ file = Application.fetch_env!(:grading_server, :answer_store_file)
+ file = Path.join([:code.priv_dir(:grading_server), file])
+
+ {:ok, modules} = YamlElixir.read_from_file(file)
+
+ modules
+ |> Enum.map(&map_module/1)
+ |> List.flatten()
+ end
+
+ defp map_module({module_name, answers}) do
+ [_, module_id] = String.split(module_name, "module_")
+ module_id = String.to_integer(module_id)
+
+ Enum.map(answers, fn data ->
+ %Answer{
+ module_id: module_id,
+ answer: data["answer"],
+ help_text: data["help_text"],
+ question_id: data["question_id"]
+ }
+ end)
+ end
+end
diff --git a/grading_server/lib/grading_server/answers.ex b/grading_server/lib/grading_server/answers.ex
new file mode 100644
index 0000000..e041da3
--- /dev/null
+++ b/grading_server/lib/grading_server/answers.ex
@@ -0,0 +1,26 @@
+defmodule GradingServer.Answers do
+ @moduledoc """
+ This module is responsible for checking if an answer is correct.
+ It uses the `AnswerStore` to fetch the answer and compares if it is correct or not.
+ """
+ alias GradingServer.AnswerStore
+ alias GradingServer.Answer
+
+ @doc """
+ Checks if the given answer is correct.
+ """
+ @spec check(integer(), integer(), String.t()) :: :correct | {:incorrect, String.t()}
+ def check(module_id, question_id, answer) do
+ case AnswerStore.get_answer(module_id, question_id) do
+ nil ->
+ {:incorrect, "Question not found"}
+
+ %{answer: correct_answer, help_text: help_text} ->
+ if String.trim(answer) == String.trim(correct_answer) do
+ :correct
+ else
+ {:incorrect, help_text}
+ end
+ end
+ end
+end
diff --git a/grading_server/lib/grading_server/application.ex b/grading_server/lib/grading_server/application.ex
new file mode 100644
index 0000000..e44c705
--- /dev/null
+++ b/grading_server/lib/grading_server/application.ex
@@ -0,0 +1,35 @@
+defmodule GradingServer.Application do
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ @moduledoc false
+
+ use Application
+
+ @impl true
+ def start(_type, _args) do
+ children = [
+ # Start the Telemetry supervisor
+ GradingServerWeb.Telemetry,
+ GradingServer.AnswerStore,
+ # Start the PubSub system
+ {Phoenix.PubSub, name: GradingServer.PubSub},
+ # Start the Endpoint (http/https)
+ GradingServerWeb.Endpoint
+ # Start a worker by calling: GradingServer.Worker.start_link(arg)
+ # {GradingServer.Worker, arg}
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: GradingServer.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ @impl true
+ def config_change(changed, _new, removed) do
+ GradingServerWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/grading_server/lib/grading_server_web.ex b/grading_server/lib/grading_server_web.ex
new file mode 100644
index 0000000..a348443
--- /dev/null
+++ b/grading_server/lib/grading_server_web.ex
@@ -0,0 +1,75 @@
+defmodule GradingServerWeb do
+ @moduledoc """
+ The entrypoint for defining your web interface, such
+ as controllers, views, channels and so on.
+
+ This can be used in your application as:
+
+ use GradingServerWeb, :controller
+ use GradingServerWeb, :view
+
+ The definitions below will be executed for every view,
+ controller, etc, so keep them short and clean, focused
+ on imports, uses and aliases.
+
+ Do NOT define functions inside the quoted expressions
+ below. Instead, define any helper function in modules
+ and import those modules here.
+ """
+
+ def controller do
+ quote do
+ use Phoenix.Controller, namespace: GradingServerWeb
+
+ import Plug.Conn
+ alias GradingServerWeb.Router.Helpers, as: Routes
+ end
+ end
+
+ def view do
+ quote do
+ use Phoenix.View,
+ root: "lib/grading_server_web/templates",
+ namespace: GradingServerWeb
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller,
+ only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
+
+ # Include shared imports and aliases for views
+ unquote(view_helpers())
+ end
+ end
+
+ def router do
+ quote do
+ use Phoenix.Router
+
+ import Plug.Conn
+ import Phoenix.Controller
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ end
+ end
+
+ defp view_helpers do
+ quote do
+ # Import basic rendering functionality (render, render_layout, etc)
+ import Phoenix.View
+
+ import GradingServerWeb.ErrorHelpers
+ alias GradingServerWeb.Router.Helpers, as: Routes
+ end
+ end
+
+ @doc """
+ When used, dispatch to the appropriate controller/view/etc.
+ """
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end
diff --git a/grading_server/lib/grading_server_web/controllers/answer_controller.ex b/grading_server/lib/grading_server_web/controllers/answer_controller.ex
new file mode 100644
index 0000000..c00e825
--- /dev/null
+++ b/grading_server/lib/grading_server_web/controllers/answer_controller.ex
@@ -0,0 +1,45 @@
+defmodule GradingServerWeb.AnswerController do
+ use GradingServerWeb, :controller
+
+ alias GradingServer.Answers
+
+ action_fallback(GradingServerWeb.FallbackController)
+
+ def index(conn, _params) do
+ render(conn, "index.json")
+ end
+
+ def check(conn, %{"question_id" => question_id} = payload) when is_binary(question_id) do
+ case Integer.parse(question_id) do
+ :error ->
+ render(conn, "no_answer.json", question_id: question_id)
+
+ {question_id, _} ->
+ check(conn, %{payload | "question_id" => question_id})
+ end
+ end
+
+ def check(conn, %{"module_id" => module_id} = payload) when is_binary(module_id) do
+ case Integer.parse(module_id) do
+ :error ->
+ render(conn, "no_answer.json", module_id: module_id)
+
+ {module_id, _} ->
+ check(conn, %{payload | "module_id" => module_id})
+ end
+ end
+
+ def check(conn, %{"question_id" => question_id, "answer" => answer, "module_id" => module_id}) do
+ case Answers.check(module_id, question_id, answer) do
+ :correct ->
+ render(conn, "correct.json", question_id: question_id, module_id: module_id)
+
+ {:incorrect, help_text} ->
+ render(conn, "incorrect.json",
+ question_id: question_id,
+ module_id: module_id,
+ help_text: help_text
+ )
+ end
+ end
+end
diff --git a/grading_server/lib/grading_server_web/controllers/default_controller.ex b/grading_server/lib/grading_server_web/controllers/default_controller.ex
new file mode 100644
index 0000000..a9406cb
--- /dev/null
+++ b/grading_server/lib/grading_server_web/controllers/default_controller.ex
@@ -0,0 +1,7 @@
+defmodule GradingServerWeb.DefaultController do
+ use GradingServerWeb, :controller
+
+ def index(conn, _params) do
+ text conn, "This is the Elixir Secure Coding Training - Grading Server hosted by Podium for free, so be nice!"
+ end
+end
diff --git a/grading_server/lib/grading_server_web/controllers/fallback_controller.ex b/grading_server/lib/grading_server_web/controllers/fallback_controller.ex
new file mode 100644
index 0000000..19e5c63
--- /dev/null
+++ b/grading_server/lib/grading_server_web/controllers/fallback_controller.ex
@@ -0,0 +1,16 @@
+defmodule GradingServerWeb.FallbackController do
+ @moduledoc """
+ Translates controller action results into valid `Plug.Conn` responses.
+
+ See `Phoenix.Controller.action_fallback/1` for more details.
+ """
+ use GradingServerWeb, :controller
+
+ # This clause is an example of how to handle resources that cannot be found.
+ def call(conn, {:error, :not_found}) do
+ conn
+ |> put_status(:not_found)
+ |> put_view(GradingServerWeb.ErrorView)
+ |> render(:"404")
+ end
+end
diff --git a/grading_server/lib/grading_server_web/endpoint.ex b/grading_server/lib/grading_server_web/endpoint.ex
new file mode 100644
index 0000000..e48ea3d
--- /dev/null
+++ b/grading_server/lib/grading_server_web/endpoint.ex
@@ -0,0 +1,50 @@
+defmodule GradingServerWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :grading_server
+
+ # The session will be stored in the cookie and signed,
+ # this means its contents can be read but not tampered with.
+ # Set :encryption_salt if you would also like to encrypt it.
+ @session_options [
+ store: :cookie,
+ key: "_grading_server_key",
+ signing_salt: "5GyD2/sm"
+ ]
+
+ socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
+
+ # Serve at "/" the static files from "priv/static" directory.
+ #
+ # You should set gzip to true if you are running phx.digest
+ # when deploying your static files in production.
+ plug(Plug.Static,
+ at: "/",
+ from: :grading_server,
+ gzip: false,
+ only: ~w(assets fonts images favicon.ico robots.txt)
+ )
+
+ # Code reloading can be explicitly enabled under the
+ # :code_reloader configuration of your endpoint.
+ if code_reloading? do
+ plug(Phoenix.CodeReloader)
+ end
+
+ plug(Phoenix.LiveDashboard.RequestLogger,
+ param_key: "request_logger",
+ cookie_key: "request_logger"
+ )
+
+ plug(Plug.RequestId)
+ plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
+
+ plug(Plug.Parsers,
+ parsers: [:urlencoded, :multipart, :json],
+ pass: ["*/*"],
+ json_decoder: Phoenix.json_library()
+ )
+
+ plug(Plug.MethodOverride)
+ plug(Plug.Head)
+ plug(Plug.Session, @session_options)
+ plug(GradingServerWeb.Router)
+end
diff --git a/grading_server/lib/grading_server_web/router.ex b/grading_server/lib/grading_server_web/router.ex
new file mode 100644
index 0000000..55dd0a8
--- /dev/null
+++ b/grading_server/lib/grading_server_web/router.ex
@@ -0,0 +1,35 @@
+defmodule GradingServerWeb.Router do
+ use GradingServerWeb, :router
+
+ pipeline :api do
+ plug(:accepts, ["json"])
+ # plug SimpleTokenAuthentication
+ end
+
+ scope "/api", GradingServerWeb do
+ pipe_through(:api)
+ get("/", DefaultController, :index)
+ post("/answers/check", AnswerController, :check)
+ end
+
+ pipeline :browser do
+ plug(:accepts, ["html"])
+ plug(:put_secure_browser_headers, %{"content-security-policy" => "default-src 'self'"})
+ end
+
+ scope "/", GradingServerWeb do
+ pipe_through(:browser)
+ get("/", DefaultController, :index)
+ end
+
+ # Enables LiveDashboard only for development
+ if Mix.env() in [:dev, :test] do
+ import Phoenix.LiveDashboard.Router
+
+ scope "/" do
+ pipe_through([:fetch_session, :protect_from_forgery])
+
+ live_dashboard("/dashboard", metrics: GradingServerWeb.Telemetry)
+ end
+ end
+end
diff --git a/grading_server/lib/grading_server_web/telemetry.ex b/grading_server/lib/grading_server_web/telemetry.ex
new file mode 100644
index 0000000..dc61c3a
--- /dev/null
+++ b/grading_server/lib/grading_server_web/telemetry.ex
@@ -0,0 +1,48 @@
+defmodule GradingServerWeb.Telemetry do
+ @moduledoc false
+ use Supervisor
+ import Telemetry.Metrics
+
+ def start_link(arg) do
+ Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
+ end
+
+ @impl true
+ def init(_arg) do
+ children = [
+ # Telemetry poller will execute the given period measurements
+ # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
+ {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
+ # Add reporters as children of your supervision tree.
+ # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
+ ]
+
+ Supervisor.init(children, strategy: :one_for_one)
+ end
+
+ def metrics do
+ [
+ # Phoenix Metrics
+ summary("phoenix.endpoint.stop.duration",
+ unit: {:native, :millisecond}
+ ),
+ summary("phoenix.router_dispatch.stop.duration",
+ tags: [:route],
+ unit: {:native, :millisecond}
+ ),
+ # VM Metrics
+ summary("vm.memory.total", unit: {:byte, :kilobyte}),
+ summary("vm.total_run_queue_lengths.total"),
+ summary("vm.total_run_queue_lengths.cpu"),
+ summary("vm.total_run_queue_lengths.io")
+ ]
+ end
+
+ defp periodic_measurements do
+ [
+ # A module, function and arguments to be invoked periodically.
+ # This function must call :telemetry.execute/3 and a metric must be added above.
+ # {GradingServerWeb, :count_users, []}
+ ]
+ end
+end
diff --git a/grading_server/lib/grading_server_web/views/answer_view.ex b/grading_server/lib/grading_server_web/views/answer_view.ex
new file mode 100644
index 0000000..db972a9
--- /dev/null
+++ b/grading_server/lib/grading_server_web/views/answer_view.ex
@@ -0,0 +1,25 @@
+defmodule GradingServerWeb.AnswerView do
+ use GradingServerWeb, :view
+ alias GradingServerWeb.AnswerView
+
+ def render("index.json", _assigns) do
+ %{
+ errors: %{
+ detail:
+ "Nice try, you didn't think it would be that easy to get all the answers - did you?"
+ }
+ }
+ end
+
+ def render("correct.json", %{question_id: question_id, module_id: module_id}) do
+ %{module_id: module_id, question_id: question_id, correct: true, help_text: nil}
+ end
+
+ def render("incorrect.json", %{
+ question_id: question_id,
+ module_id: module_id,
+ help_text: help_text
+ }) do
+ %{module_id: module_id, question_id: question_id, correct: false, help_text: help_text}
+ end
+end
diff --git a/grading_server/lib/grading_server_web/views/error_helpers.ex b/grading_server/lib/grading_server_web/views/error_helpers.ex
new file mode 100644
index 0000000..a341b0e
--- /dev/null
+++ b/grading_server/lib/grading_server_web/views/error_helpers.ex
@@ -0,0 +1,16 @@
+defmodule GradingServerWeb.ErrorHelpers do
+ @moduledoc """
+ Conveniences for translating and building error messages.
+ """
+
+ @doc """
+ Translates an error message.
+ """
+ def translate_error({msg, opts}) do
+ # Because the error messages we show in our forms and APIs
+ # are defined inside Ecto, we need to translate them dynamically.
+ Enum.reduce(opts, msg, fn {key, value}, acc ->
+ String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
+ end)
+ end
+end
diff --git a/grading_server/lib/grading_server_web/views/error_view.ex b/grading_server/lib/grading_server_web/views/error_view.ex
new file mode 100644
index 0000000..8b4b6c5
--- /dev/null
+++ b/grading_server/lib/grading_server_web/views/error_view.ex
@@ -0,0 +1,16 @@
+defmodule GradingServerWeb.ErrorView do
+ use GradingServerWeb, :view
+
+ # If you want to customize a particular status code
+ # for a certain format, you may uncomment below.
+ # def render("500.json", _assigns) do
+ # %{errors: %{detail: "Internal Server Error"}}
+ # end
+
+ # By default, Phoenix returns the status message from
+ # the template name. For example, "404.json" becomes
+ # "Not Found".
+ def template_not_found(template, _assigns) do
+ %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
+ end
+end
diff --git a/grading_server/mix.exs b/grading_server/mix.exs
new file mode 100644
index 0000000..a0e4715
--- /dev/null
+++ b/grading_server/mix.exs
@@ -0,0 +1,62 @@
+defmodule GradingServer.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :grading_server,
+ version: "0.1.0",
+ elixir: "~> 1.12",
+ elixirc_paths: elixirc_paths(Mix.env()),
+ compilers: Mix.compilers(),
+ start_permanent: Mix.env() == :prod,
+ aliases: aliases(),
+ deps: deps()
+ ]
+ end
+
+ # Configuration for the OTP application.
+ #
+ # Type `mix help compile.app` for more information.
+ def application do
+ [
+ mod: {GradingServer.Application, []},
+ extra_applications: [:logger, :runtime_tools]
+ ]
+ end
+
+ # Specifies which paths to compile per environment.
+ defp elixirc_paths(:test), do: ["lib", "test/support"]
+ defp elixirc_paths(_), do: ["lib"]
+
+ # Specifies your project dependencies.
+ #
+ # Type `mix help deps` for examples and options.
+ defp deps do
+ [
+ {:credo, "~> 1.7"},
+ {:phoenix, "~> 1.6.11"},
+ {:yaml_elixir, "~> 2.9"},
+ {:phoenix_live_dashboard, "~> 0.6"},
+ {:telemetry_metrics, "~> 0.6"},
+ {:telemetry_poller, "~> 1.0"},
+ {:jason, "~> 1.4"},
+ # {:jason, "~> 1.4"}, uncomment when {:jason, "1.5.0"} is out for GA
+ {:plug_cowboy, "~> 2.5"},
+ {:sobelow, "~> 0.12"},
+ {:simple_token_authentication, "~> 0.7.0"}
+ ]
+ end
+
+ # Aliases are shortcuts or tasks specific to the current project.
+ # For example, to install project dependencies and perform other setup tasks, run:
+ #
+ # $ mix setup
+ #
+ # See the documentation for `Mix` for more info on aliases.
+ defp aliases do
+ [
+ setup: ["deps.get"],
+ test: ["test"]
+ ]
+ end
+end
diff --git a/grading_server/mix.lock b/grading_server/mix.lock
new file mode 100644
index 0000000..289515f
--- /dev/null
+++ b/grading_server/mix.lock
@@ -0,0 +1,36 @@
+%{
+ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
+ "castore": {:hex, :castore, "1.0.0", "c25cd0794c054ebe6908a86820c8b92b5695814479ec95eeff35192720b71eec", [:mix], [], "hexpm", "577d0e855983a97ca1dfa33cbb8a3b6ece6767397ffb4861514343b078fc284b"},
+ "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
+ "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
+ "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
+ "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
+ "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
+ "db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"},
+ "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
+ "ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"},
+ "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
+ "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
+ "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
+ "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
+ "phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
+ "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.11", "c50eac83dae6b5488859180422dfb27b2c609de87f4aa5b9c926ecd0501cd44f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76c99a0ffb47cd95bf06a917e74f282a603f3e77b00375f3c2dd95110971b102"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
+ "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
+ "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
+ "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
+ "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
+ "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
+ "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
+ "simple_token_authentication": {:hex, :simple_token_authentication, "0.7.0", "5621af0367ee37d71b90d0d9346fdde097ac1daab18c070252e59db620de2e3e", [:mix], [{:plug, ">= 1.3.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6100de9482b04603b22ecba0b4f93beab1827fdf9dfda6f0dab59830f6815357"},
+ "sobelow": {:hex, :sobelow, "0.12.2", "45f4d500e09f95fdb5a7b94c2838d6b26625828751d9f1127174055a78542cf5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2f0b617dce551db651145662b84c8da4f158e7abe049a76daaaae2282df01c5d"},
+ "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
+ "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
+ "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
+ "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
+}
diff --git a/grading_server/priv/answers.yml b/grading_server/priv/answers.yml
new file mode 100644
index 0000000..9817b88
--- /dev/null
+++ b/grading_server/priv/answers.yml
@@ -0,0 +1,44 @@
+# Answers for modules should be named `module_MODULE-NUMBER`
+
+module_1:
+ - question_id: 1
+ help_text: "This is helpful text that can be used."
+ answer: "Not a real answer, but an example."
+
+module_2:
+ - question_id: 1
+ help_text: ""
+ answer: ""
+ - question_id: 2
+ help_text: ""
+ answer: ""
+
+module_3:
+ - question_id: 1
+ help_text: ""
+ answer: ""
+
+module_5:
+ - question_id: 1
+ help_text: ""
+ answer: ""
+
+module_6:
+ - question_id: 1
+ help_text: ""
+ answer: ""
+
+module_7:
+ - question_id: 1
+ help_text: ""
+ answer: ""
+
+module_8:
+ - question_id: 1
+ help_text: ""
+ answer: ""
+
+module_9:
+ - question_id: 1
+ help_text: ""
+ answer: ""
diff --git a/grading_server/priv/test_answers.yml b/grading_server/priv/test_answers.yml
new file mode 100644
index 0000000..d92090f
--- /dev/null
+++ b/grading_server/priv/test_answers.yml
@@ -0,0 +1,7 @@
+answers:
+ - question_id: 1
+ help_text: "This is helpful text that can be used."
+ answer: "Not a real answer, but an example."
+ - question_id: 2
+ help_text: "This is helpful text that can be used."
+ answer: "Not a real answer, but an example."
diff --git a/grading_server/test/grading_server_web/controllers/answer_controller_test.exs b/grading_server/test/grading_server_web/controllers/answer_controller_test.exs
new file mode 100644
index 0000000..91957ce
--- /dev/null
+++ b/grading_server/test/grading_server_web/controllers/answer_controller_test.exs
@@ -0,0 +1,10 @@
+defmodule GradingServerWeb.AnswerControllerTest do
+ use GradingServerWeb.ConnCase
+
+ setup %{conn: conn} do
+ {:ok, conn: put_req_header(conn, "accept", "application/json")}
+ end
+
+ describe "index" do
+ end
+end
diff --git a/grading_server/test/grading_server_web/views/error_view_test.exs b/grading_server/test/grading_server_web/views/error_view_test.exs
new file mode 100644
index 0000000..f2b2033
--- /dev/null
+++ b/grading_server/test/grading_server_web/views/error_view_test.exs
@@ -0,0 +1,15 @@
+defmodule GradingServerWeb.ErrorViewTest do
+ use GradingServerWeb.ConnCase, async: true
+
+ # Bring render/3 and render_to_string/3 for testing custom views
+ import Phoenix.View
+
+ test "renders 404.json" do
+ assert render(GradingServerWeb.ErrorView, "404.json", []) == %{errors: %{detail: "Not Found"}}
+ end
+
+ test "renders 500.json" do
+ assert render(GradingServerWeb.ErrorView, "500.json", []) ==
+ %{errors: %{detail: "Internal Server Error"}}
+ end
+end
diff --git a/grading_server/test/support/conn_case.ex b/grading_server/test/support/conn_case.ex
new file mode 100644
index 0000000..662c02c
--- /dev/null
+++ b/grading_server/test/support/conn_case.ex
@@ -0,0 +1,37 @@
+defmodule GradingServerWeb.ConnCase do
+ @moduledoc """
+ This module defines the test case to be used by
+ tests that require setting up a connection.
+
+ Such tests rely on `Phoenix.ConnTest` and also
+ import other functionality to make it easier
+ to build common data structures and query the data layer.
+
+ Finally, if the test case interacts with the database,
+ we enable the SQL sandbox, so changes done to the database
+ are reverted at the end of every test. If you are using
+ PostgreSQL, you can even run database tests asynchronously
+ by setting `use GradingServerWeb.ConnCase, async: true`, although
+ this option is not recommended for other databases.
+ """
+
+ use ExUnit.CaseTemplate
+
+ using do
+ quote do
+ # Import conveniences for testing with connections
+ import Plug.Conn
+ import Phoenix.ConnTest
+ import GradingServerWeb.ConnCase
+
+ alias GradingServerWeb.Router.Helpers, as: Routes
+
+ # The default endpoint for testing
+ @endpoint GradingServerWeb.Endpoint
+ end
+ end
+
+ setup _tags do
+ {:ok, conn: Phoenix.ConnTest.build_conn()}
+ end
+end
diff --git a/grading_server/test/support/fixtures/key_fixtures.ex b/grading_server/test/support/fixtures/key_fixtures.ex
new file mode 100644
index 0000000..a1a2ab3
--- /dev/null
+++ b/grading_server/test/support/fixtures/key_fixtures.ex
@@ -0,0 +1,22 @@
+defmodule GradingServer.KeyFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `GradingServer.Key` context.
+ """
+
+ @doc """
+ Generate a answer.
+ """
+ def answer_fixture(attrs \\ %{}) do
+ {:ok, answer} =
+ attrs
+ |> Enum.into(%{
+ answer: "some answer",
+ help_text: "some help_text",
+ question_id: 42
+ })
+ |> GradingServer.Key.create_answer()
+
+ answer
+ end
+end
diff --git a/grading_server/test/test_helper.exs b/grading_server/test/test_helper.exs
new file mode 100644
index 0000000..869559e
--- /dev/null
+++ b/grading_server/test/test_helper.exs
@@ -0,0 +1 @@
+ExUnit.start()
diff --git a/modules/2-owasp.livemd b/modules/2-owasp.livemd
index 6f2fc52..e250dab 100644
--- a/modules/2-owasp.livemd
+++ b/modules/2-owasp.livemd
@@ -2,6 +2,7 @@
```elixir
Mix.install([
+ {:grading_client, path: "#{__DIR__}/grading_client"},
:bcrypt_elixir,
:httpoison,
{:absinthe, "~> 1.7.0"},
@@ -11,6 +12,7 @@ Mix.install([
md5_hash = :crypto.hash(:md5, "users_password")
bcrypt_salted_hash = Bcrypt.hash_pwd_salt("users_password")
+
:ok
```
@@ -24,7 +26,7 @@ The [Open Web Application Security Project (OWASP)](https://owasp.org/) is a non
They are an open community dedicated to enabling organizations to conceive, develop, acquire, operate, and maintain applications that can be trusted. All of their projects, tools, documents, forums, and chapters are free and open to anyone interested in improving application security.
-Having been in the Application Security space since 2001, they are somewhat of the defacto standard when it comes to best practices. It is *highly* encouraged to give their site a visit and look around at available materials!
+Having been in the Application Security space since 2001, they are somewhat of the defacto standard when it comes to best practices. It is _highly_ encouraged to give their site a visit and look around at available materials!
## Tables of Contents
@@ -77,7 +79,7 @@ Notable CWEs included are CWE-259: Use of Hard-coded Password, CWE-327: Broken o
### Prevention
-* Don't store sensitive data unnecessarily.
+* Don't store sensitive data unnecessarily.
* Discard it as soon as possible or use PCI DSS compliant tokenization or even truncation.
* Data that is not retained cannot be stolen.
* Ensure up-to-date and strong standard algorithms, protocols, and keys are in place; use proper key management.
@@ -87,7 +89,7 @@ Notable CWEs included are CWE-259: Use of Hard-coded Password, CWE-327: Broken o
* Make sure to encrypt all sensitive data at rest.
* Disable caching for responses that contain sensitive data.
* Do not use legacy protocols such as FTP and SMTP.
-* Store passwords using strong and salted hashing functions with a delay factor.
+* Store passwords using strong and salted hashing functions with a delay factor.
* Argon2, scrypt, bcrypt or PBKDF2 are good examples.
* Always use [authenticated encryption](https://en.wikipedia.org/wiki/Authenticated_encryption) instead of just encryption.
* Ensure that cryptographic randomness is used where appropriate, and that it has not been seeded in a predictable way or with low entropy.
@@ -99,7 +101,7 @@ Notable CWEs included are CWE-259: Use of Hard-coded Password, CWE-327: Broken o
**Which of the following functions is performing password comparisons in the most secure way?**
-*Please uncomment the function call that you believe is correct.*
+_Please uncomment the function call that you believe is correct._
```elixir
defmodule PasswordCompare do
@@ -213,10 +215,10 @@ Vulnerable Components are a known issue that we struggle to test and assess risk
You are likely vulnerable:
-* If you do not know the versions of all components you use (both client-side and server-side).
+* If you do not know the versions of all components you use (both client-side and server-side).
* This includes components you directly use as well as nested dependencies.
-* If the software is vulnerable, unsupported, or out of date.
- * This includes:
+* If the software is vulnerable, unsupported, or out of date.
+ * This includes:
* Operating System
* Web/Application Server
* Databases
@@ -224,7 +226,7 @@ You are likely vulnerable:
* Runtime Environments
* Libraries / Dependencies
* If you do not scan for vulnerabilities regularly and subscribe to security bulletins related to the components you use.
-* If you do not fix or upgrade the underlying platform, frameworks, and dependencies in a risk-based, timely fashion.
+* If you do not fix or upgrade the underlying platform, frameworks, and dependencies in a risk-based, timely fashion.
* This commonly happens in environments when patching is a monthly or quarterly task under change control, leaving organizations open to days or months of unnecessary exposure to fixed vulnerabilities.
* If software developers do not test the compatibility of updated, upgraded, or patched libraries.
@@ -244,9 +246,9 @@ Notable CWE included is CWE-1104: Use of Unmaintained Third-Party Components
**Which of the outdated components currently installed is vulnerable?**
-*Please change the atom below to the name of the vulnerable package installed in this Livebook AND update the afflicted package.*
+_Please change the atom below to the name of the vulnerable package installed in this Livebook AND update the afflicted package._
-*HINT: Installed dependencies can be found at the very top, it was the very first cell you ran.*
+_HINT: Installed dependencies can be found at the very top, it was the very first cell you ran._
```elixir
# CHANGE ME
@@ -274,7 +276,7 @@ Confirmation of the user's identity, authentication, and session management is c
* Has missing or ineffective multi-factor authentication.
* Exposes session identifier in the URL.
* Reuse of a session identifier after successful login.
-* Does not correctly invalidate Session IDs.
+* Does not correctly invalidate Session IDs.
* User sessions or authentication tokens (mainly single sign-on (SSO) tokens) aren't properly invalidated during logout or a period of inactivity.
Notable CWEs included are CWE-297: Improper Validation of Certificate with Host Mismatch, CWE-287: Improper Authentication, and CWE-384: Session Fixation.
@@ -286,9 +288,9 @@ Notable CWEs included are CWE-297: Improper Validation of Certificate with Host
* Implement mechanisms for proactively performing weak password checks and enforce users to set strong passwords for their accounts.
* Align password length, complexity, and rotation policies with industry standards.
* Ensure registration, credential recovery, and API pathways are hardened against account enumeration attacks by using the same messages for all outcomes.
-* Limit or increasingly delay failed login attempts.
+* Limit or increasingly delay failed login attempts.
* Log all failures and alert the Security team when credential stuffing, brute force, or other attacks are detected.
-* Use a server-side, secure, built-in session manager that generates a new random session ID with high entropy after login.
+* Use a server-side, secure, built-in session manager that generates a new random session ID with high entropy after login.
* Session identifier should not be in the URL, be securely stored, and invalidated after logout, idle,
diff --git a/modules/3-ssdlc.livemd b/modules/3-ssdlc.livemd
index 98bf580..6afce3a 100644
--- a/modules/3-ssdlc.livemd
+++ b/modules/3-ssdlc.livemd
@@ -1,5 +1,13 @@
# ESCT: Part 3 - Secure SDLC Concepts
+```elixir
+Mix.install([
+ {:grading_client, path: "#{__DIR__}/grading_client"}
+])
+
+:ok
+```
+
## Introduction
Welcome to Part 3! This section is dedicated to discussing some of the more abstract, non-vulnerability related parts of Application Security. Concepts to keep in mind as you work your craft at building code.
@@ -35,7 +43,7 @@ A very easy way to prevent secrets being added to go though is to access them vi
**Remove the hard-coded secret from the code sample below and replace it with an environment variable named `envar_secret`.**
-*Use `System.get_env/1` on line 2.*
+_Use `System.get_env/1` on line 2._
```elixir
# let's assume there is an environment variable named 'envar_secret'
@@ -85,11 +93,11 @@ More often than not, rate limiting should be as specific as possible. For instan
### Principle of Least Privilege
-Sometimes known as the Principle of Minimal Privilege or the Principle of Least Authority, the Principle of Least Privilege (PoLP) means that every entity* is only strictly given the essential privileges needed to perform its requirement.
+Sometimes known as the Principle of Minimal Privilege or the Principle of Least Authority, the Principle of Least Privilege (PoLP) means that every entity\* is only strictly given the essential privileges needed to perform its requirement.
E.g. A script that executes on a cron schedule that monitors the output of a log file only needs read privileges and should not be given write privileges.
-**Entity: generic term for an arbitrary process, user, program, etc. found within a Data System*
+\*_Entity: generic term for an arbitrary process, user, program, etc. found within a Data System_
#### Benefits of the Principle
@@ -104,63 +112,66 @@ Zero Trust is not only about controlling user access, but requires strict contro
### Microsegmentation
-Microsegmentation is the practice of breaking up security perimeters into small zones to maintain separate access for separate parts of the network. Some of the benefits of doing so are:
-* Granular Access Policies - we can create super specific policies for access to each segment!
-* Targeted Security Controls - we can develop each micro-perimeter to specifically target the security risks and vulnerabilities of the resources in that micro-segment!
-* Establishing Identities and Trust - we can implement, monitor, and control the “never trust, always verify” principle much easier!
+Microsegmentation is the practice of breaking up security perimeters into small zones to maintain separate access for separate parts of the network. Some of the benefits of doing so are:
+
+* Granular Access Policies - we can create super specific policies for access to each segment!
+* Targeted Security Controls - we can develop each micro-perimeter to specifically target the security risks and vulnerabilities of the resources in that micro-segment!
+* Establishing Identities and Trust - we can implement, monitor, and control the “never trust, always verify” principle much easier!
-### Preventing Lateral Movement
+### Preventing Lateral Movement
-Zero Trust is designed to contain attackers so that they can not move laterally. You may be asking what does that even mean? In network security, “lateral movement” is when an attacker moves within a network after gaining access to it, which can be very difficult to detect.
+Zero Trust is designed to contain attackers so that they can not move laterally. You may be asking what does that even mean? In network security, “lateral movement” is when an attacker moves within a network after gaining access to it, which can be very difficult to detect.
-Zero Trust helps contain attackers because the access is segmented and has to be reestablished periodically, limiting them from moving across to other microsegments within the network.
+Zero Trust helps contain attackers because the access is segmented and has to be reestablished periodically, limiting them from moving across to other microsegments within the network.
### Multi Factor Authentication
-It's no surprise that MFA is a core part of the Zero Trust Model. Systems using MFA require more than one piece of evidence to authenticate a user, with the most common form being a one time password (OTP).
+It's no surprise that MFA is a core part of the Zero Trust Model. Systems using MFA require more than one piece of evidence to authenticate a user, with the most common form being a one time password (OTP).
### Resource
+
1. https://www.cloudflare.com/learning/security/glossary/what-is-zero-trust/
## Defense In Depth
-Defense in depth is a security approach of having defense mechanisms in a layered approach to protect valuable assets. Castles take a similar approach, where they have a moat, ramparts, towers, and drawbridges instead of just one wall as protection.
+Defense in depth is a security approach of having defense mechanisms in a layered approach to protect valuable assets. Castles take a similar approach, where they have a moat, ramparts, towers, and drawbridges instead of just one wall as protection.
+
+An example of developing a web application using defense in depth could be:
-An example of developing a web application using defense in depth could be:
* The developers (like yourself) receive secure coding training
* The codebase is checked automatically for vulnerabilities using Semgrep
* The codebase is also checked for outdated dependencies using Dependabot
-* The application is regularly tested by the internal security team
+* The application is regularly tested by the internal security team
* Multiple development environments are used such as Develpoment, Staging, and Production
-
+
-Using more than one of the following layers constitutes an example of defense in depth:
+Using more than one of the following layers constitutes an example of defense in depth:
-### System and Application
+### System and Application
* Authentication and password security
- * Hashing passwords
- * Multi factor authentication (MFA)
-* Encryption
-* Security Tooling
-* Security Awareness Training (sounds familiar 😉)
-* Logging and Monitoring
+ * Hashing passwords
+ * Multi factor authentication (MFA)
+* Encryption
+* Security Tooling
+* Security Awareness Training (sounds familiar 😉)
+* Logging and Monitoring
-### Network
+### Network
-* Firewalls (hardware and software)
-* Demilitarized zones (DMZ)
+* Firewalls (hardware and software)
+* Demilitarized zones (DMZ)
* Virtual Private Networks (VPN)
-### Physical
+### Physical
-* Biometrics
-* Data-centric security
-* Physical Security (such as locked server rooms)
+* Biometrics
+* Data-centric security
+* Physical Security (such as locked server rooms)
### Resource
-1. https://www.forcepoint.com/cyber-edu/defense-depth
+1. https://www.forcepoint.com/cyber-edu/defense-depth
[**<- Previous Module: OWASP**](./2-owasp.livemd) || [**Next Module: GraphQL Security ->**](./4-graphql.livemd)
diff --git a/modules/4-graphql.livemd b/modules/4-graphql.livemd
index 0c6742a..f5af286 100644
--- a/modules/4-graphql.livemd
+++ b/modules/4-graphql.livemd
@@ -1,10 +1,18 @@
# ESCT: Part 4 - GraphQL Security
+```elixir
+Mix.install([
+ {:grading_client, path: "#{__DIR__}/grading_client"}
+])
+
+:ok
+```
+
## Introduction
-GraphQL is a query language used to interact with and retrieve data from an application's data sources. Its structure is designed for flexible and precise queries that efficiently interact with complex, highly nested data sets. Using GraphQL, information is retrieved by stepping through data as if it were arranged as a group of connected nodes instead of a strictly hierarchical set up. Think more of a labyrinth than a tree.
+GraphQL is a query language used to interact with and retrieve data from an application's data sources. Its structure is designed for flexible and precise queries that efficiently interact with complex, highly nested data sets. Using GraphQL, information is retrieved by stepping through data as if it were arranged as a group of connected nodes instead of a strictly hierarchical set up. Think more of a labyrinth than a tree.
-Since GraphQL can be implemented as a component of an application's API, there are security issues common to all APIs present, as well as concerns related to characteristics of the query language itself. This module will highlight several security issues associated with GraphQL and recommendations for how to address them.
+Since GraphQL can be implemented as a component of an application's API, there are security issues common to all APIs present, as well as concerns related to characteristics of the query language itself. This module will highlight several security issues associated with GraphQL and recommendations for how to address them.
## Table of Contents
@@ -17,31 +25,32 @@ Since GraphQL can be implemented as a component of an application's API, there a
### Description
-Introspection queries are a way of enumerating a particular GraphQL implementation to discover details about the queries supported, data types available, and other information. This includes mutation names, fields specific to an organization or dataset, query parameters, and types of objects in the data source. Obtaining this information can help a user, including a malicious one, deduce and discover specifics about the data being stored.
+Introspection queries are a way of enumerating a particular GraphQL implementation to discover details about the queries supported, data types available, and other information. This includes mutation names, fields specific to an organization or dataset, query parameters, and types of objects in the data source. Obtaining this information can help a user, including a malicious one, deduce and discover specifics about the data being stored.
-If you are familiar with databases, this is similar to gathering info on the [database schema]( https://en.wikipedia.org/wiki/Database_schema) that includes information about table names, fields, database, structure etc.
+If you are familiar with databases, this is similar to gathering info on the [database schema](https://en.wikipedia.org/wiki/Database_schema) that includes information about table names, fields, database, structure etc.
Malicious actors in their information gathering/reconnaissnce efforts can leverage this information as they look for ways to attack your application and construct malicious queries and requests to expose and compromise data.
-Excessive Data Exposure is number 3 on OWASP's API Security Top 2019 and APIs with this issue return too much and/or sensitive information in response to incoming requests and queries. Although it provides a useful function for GraphQL developers, the information returned by introspection can help facilitate attack.
+Excessive Data Exposure is number 3 on OWASP's API Security Top 2019 and APIs with this issue return too much and/or sensitive information in response to incoming requests and queries. Although it provides a useful function for GraphQL developers, the information returned by introspection can help facilitate attack.
### Prevention
-The less an attacker can learn about your system or application, the more difficult (though, of course, not impossible given time and resources) it will be to identify vulnerablilities and craft exploits that could result in a successful compromise.
+The less an attacker can learn about your system or application, the more difficult (though, of course, not impossible given time and resources) it will be to identify vulnerablilities and craft exploits that could result in a successful compromise.
Taking every opportunity to add a layer of difficulty (see defense in depth section in Module 3) for malicious actors is one aspect of securing data and applications.
-Imagine trying to surprise a friend who lives in another part of the country with a complete home makeover. You've never been to their place but imagine trying to make arrangements, in secret, without having a map to their city, knowing their address, having access to a website with a street view, or having the floorplans. What if they live on the 30th floor of an apartment building, there's no street parking, and they have 4 large pet dragons?
+Imagine trying to surprise a friend who lives in another part of the country with a complete home makeover. You've never been to their place but imagine trying to make arrangements, in secret, without having a map to their city, knowing their address, having access to a website with a street view, or having the floorplans. What if they live on the 30th floor of an apartment building, there's no street parking, and they have 4 large pet dragons?
Since introspection queries provide the floorplans to your data, one step in making an attacker's job more difficult is to, if an evaluation of your application development processes determines it is not needed, disable introspection. If it is not possible to completely disable introspection, the next best defense is to limit access, by following least privilege, or implement other controls to limit exposure. Please see references for more details.
-### Example
+### Example
-[Vigil](https://github.com/podium/vigil) is an elixir package that when added to your application's dependencies, can intercept incoming GraphQL introspection requests and return an error/forbidden message to the client, instead of information about the schema.
+[Vigil](https://github.com/podium/vigil) is an elixir package that when added to your application's dependencies, can intercept incoming GraphQL introspection requests and return an error/forbidden message to the client, instead of information about the schema.
-It can also intercept responses to ensure no schema data is being leaked in any error messages shown to the client.
+It can also intercept responses to ensure no schema data is being leaked in any error messages shown to the client.
### Resources
+
1. https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/12-API_Testing/01-Testing_GraphQL
2. https://cybervelia.com/?p=736
3. https://github.com/podium/vigil
@@ -50,9 +59,9 @@ It can also intercept responses to ensure no schema data is being leaked in any
**Which of the OWASP API Security Top 10 2019 issues does disabling introspection queries address?**
-*Uncomment the line with your answer.*
+_Uncomment the line with your answer._
-```
+```elixir
# answer = :API6_2019_Mass_Assignment
# answer = :API10_2019_Insufficient_Logging_Monitoring
# answer = :API3_2019_Excessive_Data_Exposure
@@ -67,7 +76,7 @@ IO.puts(answer)
When an application responds with overly verbose error messages, it runs the risk of providing vital information to an attacker seeking to exploit the service. It is a best practice to limit the amount of meaningful information that gets sent back to any user in the event there is an issue with a service, or other application component, including APIs.
-Within the context of a GraphQL implementation, when errors occur, the server could send error messages that reveal internal details, application configurations, or data which could be used to further an attack on the application.
+Within the context of a GraphQL implementation, when errors occur, the server could send error messages that reveal internal details, application configurations, or data which could be used to further an attack on the application.
### Prevention
@@ -79,59 +88,62 @@ OWASP recommends explicitly defining and enforcing all API response payload sche
**Select the best example of a “good” error message, from the perspective of developer who is writing code that is intended to inform a user (who may or may not be a malicious actor) that the action they have attempted was unsuccessful:**
-*Uncomment the item number (1-4) with your answer*
-```
+_Uncomment the item number (1-4) with your answer_
+
+```elixir
# -------------------------------------------------------------
-# answer = :1
+# answer = :1
#
-#HTTP/2 401 Unauthorized
-#Date: Tues, 16 Aug 2022 21:06:42 GMT
-#…
-#{
-# “error”:”token expired”
-#{
+# HTTP/2 401 Unauthorized
+# Date: Tues, 16 Aug 2022 21:06:42 GMT
+# …
+# {
+# “error”:”token expired”
+# {
# -------------------------------------------------------------
# answer = :2
#
-#HTTP/2 200 OK
-#Date: Tues, 16 Aug 2021 22:06:42 GMT
-#…
-#{
-# “errors”:[
-# {
-# “locations”:[
-# {
-# “column”:2,
-# :line”:2
-# }
-# ],
-# “message”: “Parsing failed at
-# }
-# ]
-#}
+# HTTP/2 200 OK
+# Date: Tues, 16 Aug 2021 22:06:42 GMT
+# …
+# {
+# “errors”:[
+# {
+# “locations”:[
+# {
+# “column”:2,
+# :line”:2
+# }
+# ],
+# “message”: “Parsing failed at
+# }
+# ]
+# }
# --------------------------------------------------------------
# answer = :3
#
-#HTTP/2 200 OK
-#Date: Tues, 16 Aug 2022 21:06:42 GMT
-#…
-#{
-# “error”:”ID token for user 55e4cb07 at org 1234 expired”
-#{
+# HTTP/2 200 OK
+# Date: Tues, 16 Aug 2022 21:06:42 GMT
+# …
+# {
+# “error”:”ID token for user 55e4cb07 at org 1234 expired”
+# {
# ---------------------------------------------------------------
-# answer = :4
+# answer = :4
#
-#HTTP/2 404 File Not Found
-#Date: Tues, 16 Aug 2022 21:06:42 GMT
-#…
-#{
-# “error”:”/www/home/file.txt not found ”
-#{
+# HTTP/2 404 File Not Found
+# Date: Tues, 16 Aug 2022 21:06:42 GMT
+# …
+# {
+# “error”:”/www/home/file.txt not found ”
+# {
# ---------------------------------------------------------------
IO.puts(answer)
```
+
### Resources
+
1. https://github.com/OWASP/API-Security/blob/master/2019/en/src/0xa7-security-misconfiguration.md
2. https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/12-API_Testing/01-Testing_GraphQL
3. https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
@@ -140,31 +152,30 @@ IO.puts(answer)
### Description
-When building an application, it is necessary to manage the access and use of all relevant internal and external resources involved in the context of the application. This will help ensure the continued availablilty of the application and its functionality for all legitimate users and entities.
+When building an application, it is necessary to manage the access and use of all relevant internal and external resources involved in the context of the application. This will help ensure the continued availablilty of the application and its functionality for all legitimate users and entities.
-Resource exhaustion occurs when memory, processes handling application requests, network traffic transmissions, server capacity, storage, and other host operating system or device limitations are exceeded while an application is running. When resource allocation is not well managed, applications become vulnerable to negative impacts in performance, unintentional service failures, and denial of service attacks, in which a malicious actor takes advantage of resource limitations to intentionally overwhelm and crash a system.
+Resource exhaustion occurs when memory, processes handling application requests, network traffic transmissions, server capacity, storage, and other host operating system or device limitations are exceeded while an application is running. When resource allocation is not well managed, applications become vulnerable to negative impacts in performance, unintentional service failures, and denial of service attacks, in which a malicious actor takes advantage of resource limitations to intentionally overwhelm and crash a system.
-Resource exhaustion can occur inadvertently through legitimate use or could be triggered intentionally in a DoS attack by a malicious actor who sends a large number or resource intensive requests to overload the application.
+Resource exhaustion can occur inadvertently through legitimate use or could be triggered intentionally in a DoS attack by a malicious actor who sends a large number or resource intensive requests to overload the application.
The structure of GraphQL queries make it particularly succeptible to this type of attack as they can be crafted to perform long running and extensive operations, depending on the data being queried.
-In addition to strategies like rate limiting to protect APIs in general, another approach to protecting GraphQL from resource exhaustion involves anticipating the cost of a query and allocating resources based on known available capacity. The next section introduces this approach.
+In addition to strategies like rate limiting to protect APIs in general, another approach to protecting GraphQL from resource exhaustion involves anticipating the cost of a query and allocating resources based on known available capacity. The next section introduces this approach.
## Cost Theory
### Description
-Resource intensive queries, like those where a GraphQL query tries to traverse and then return a significant amount of highly nest data can cause a server/service to expend a significant amount of it's processing power and other resources. These high cost queries can render a server and therefore the application useless.
+Resource intensive queries, like those where a GraphQL query tries to traverse and then return a significant amount of highly nest data can cause a server/service to expend a significant amount of it's processing power and other resources. These high cost queries can render a server and therefore the application useless.
-One approach for implementing validation on incoming queries to determine their "cost" in terms of the resources the use. Queries are defined by how much load they place on the server/service processing the request, allowing developers to plan for how best to manage resources. This is a little like making a budget.
-
-This approach also helps implement rate limiting by establishing a query cost based on the type, operation, and expected performance of each unique GraphQL request for data, and by anticipating the load on the server.
+One approach for implementing validation on incoming queries to determine their "cost" in terms of the resources the use. Queries are defined by how much load they place on the server/service processing the request, allowing developers to plan for how best to manage resources. This is a little like making a budget.
+This approach also helps implement rate limiting by establishing a query cost based on the type, operation, and expected performance of each unique GraphQL request for data, and by anticipating the load on the server.
### Resources
-1. https://shopify.engineering/rate-limiting-graphql-apis-calculating-query-complexity
-
+1. https://shopify.engineering/rate-limiting-graphql-apis-calculating-query-complexity
+
[**<- Previous Module: Secure SDLC Concepts**](./3-ssdlc.livemd) || [**Next Module: Elixir Security ->**](./5-elixir.livemd)
diff --git a/modules/5-elixir.livemd b/modules/5-elixir.livemd
index 3df1882..b80d5f7 100644
--- a/modules/5-elixir.livemd
+++ b/modules/5-elixir.livemd
@@ -1,7 +1,14 @@
# ESCT: Part 5 - Elixir Security
+## Section
+
```elixir
-Mix.install([:benchwarmer, :kino, :plug])
+Mix.install([
+ {:grading_client, path: "#{__DIR__}/grading_client"},
+ :benchwarmer,
+ :kino,
+ :plug
+])
```
## Introduction
@@ -43,7 +50,7 @@ Beware of functions in applications/libraries that create atoms from input value
**Fix the vulnerable function below by changing the String function used on line 7.**
-*You should get a `true` result when you successfully fix the function.*
+_You should get a `true` result when you successfully fix the function._
```elixir
random_number = :rand.uniform(10000)
@@ -74,8 +81,8 @@ Deserialization of untrusted input can result in atom creation, which can lead t
* Prevent function deserialisation from untrusted input, e.g. using `Plug.Crypto.non_executable_binary_to_term/1,2`
### Resource
-1. https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/serialisation
+1. https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/serialisation
## Untrusted Code
@@ -109,7 +116,7 @@ textfield_value = Kino.Input.read(name)
**BONUS QUESTION**: How would you go about securing the code above?
-Hint: Deleting it *entirely* is a fair approach 😉
+Hint: Deleting it _entirely_ is a fair approach 😉
## Timing Attacks
@@ -137,7 +144,7 @@ Note: constant time operations tend to take longer, that is the trade off for se
**Observe the two functions outlined below, one is susceptible to timing attacks and the other uses constant time to compare strings. Change the value of `user_input` and see if you notice any time difference in the execution time.**
-*Simply uncomment the IO.puts on the last line of the code block for credit on this question.*
+_Simply uncomment the IO.puts on the last line of the code block for credit on this question._
```elixir
defmodule Susceptible do
@@ -187,7 +194,7 @@ By using expressions that do not use boolean coercion, the incorrect assumption
* Prefer `or` over `||`
* Prefer `not` over `!`
-The latter will raise a "BadBooleanError" when the function returns :ok or {:error, _}. In the interest of clarity if may even be better to use a case construct, matching explicitly on true and false.
+The latter will raise a "BadBooleanError" when the function returns :ok or {:error, \_}. In the interest of clarity if may even be better to use a case construct, matching explicitly on true and false.
### Resources
@@ -197,7 +204,7 @@ The latter will raise a "BadBooleanError" when the function returns :ok or {:err
**The function `SecurityCheck` below does not return a truthy value but is treated as such in both of the commented out function calls, which if statement is the correct way to call this function?**
-*Uncomment the if statement that uses the correct boolean comparison.*
+_Uncomment the if statement that uses the correct boolean comparison._
```elixir
defmodule SecurityCheck do
@@ -234,7 +241,7 @@ While it's obvious that we don't want data of this nature to get exposed, let's
Exceptions may result in console or log output that includes a stack trace. Mostly a stack trace shows the module/function/arity and the filename/line where the exception occurred, but for the function at the top of the stack the actual list of arguments may be included instead of the function arity.
-To prevent sensitive data from leaking in a stack trace, the value may be wrapped in a closure: a zero-arity anonymous function. The inner value can be easily unwrapped where it is needed by invoking the function. If an error occurs and function arguments are written to the console or a log, it is shown as `#Fun<...>` or `#Function<...>`.
+To prevent sensitive data from leaking in a stack trace, the value may be wrapped in a closure: a zero-arity anonymous function. The inner value can be easily unwrapped where it is needed by invoking the function. If an error occurs and function arguments are written to the console or a log, it is shown as `#Fun<...>` or `#Function<...>`.
Secrets wrapped in a closure are also safe from introspection using [Observer](https://elixir-lang.org/getting-started/debugging.html#observer) and from being written to crash dumps.
@@ -265,7 +272,7 @@ defp prune_stacktrace(stacktrace), do: stacktrace
## ETS Tables
-[ETS tables](https://elixir-lang.org/getting-started/mix-otp/ets.html) are commonly used as a go to caching mechanism in-app. But did you know that they can be declared as private (through the use of the `:private` option when instantiating them)?
+[ETS tables](https://elixir-lang.org/getting-started/mix-otp/ets.html) are commonly used as a go to caching mechanism in-app. But did you know that they can be declared as private (through the use of the `:private` option when instantiating them)?
This prevents the table from being read by other processes, such as remote shell sessions. Private tables are also not visible in Observer.
@@ -274,7 +281,8 @@ This prevents the table from being read by other processes, such as remote shell
**We have decided that we do not want this ETS table to be read from other processes, so try making it private:**
```elixir
-secret_table = :ets.new(:secret_table, [:public]) # ONLY EDIT THIS LINE
+# ONLY EDIT THIS LINE
+secret_table = :ets.new(:secret_table, [:public])
:ets.info(secret_table)[:protection]
```
@@ -282,6 +290,4 @@ secret_table = :ets.new(:secret_table, [:public]) # ONLY EDIT THIS LINE
1. https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/sensitive_data.html
-
-
[**<- Previous Module: GraphQL Security**](./4-graphql.livemd) || [**Next Module: Cookie Security ->**](./6-cookies.livemd)
diff --git a/modules/6-cookies.livemd b/modules/6-cookies.livemd
index 7a0904a..c261b9a 100644
--- a/modules/6-cookies.livemd
+++ b/modules/6-cookies.livemd
@@ -1,11 +1,16 @@
# ESCT: Part 6 - Cookie Security
```elixir
-Mix.install([:phoenix, :plug])
+Mix.install([
+ {:grading_client, path: "#{__DIR__}/grading_client"},
+ :phoenix,
+ :plug
+])
alias Phoenix.ConnTest
alias Plug
conn = ConnTest.build_conn()
+
:ok
```
@@ -53,7 +58,7 @@ The Domain attribute specifies which hosts can receive a cookie.
* If unspecified, the attribute defaults to the same host that set the cookie, excluding subdomains.
* If Domain is specified, then subdomains are always included.
-Therefore, specifying Domain is *less* restrictive than omitting it. However, it can be helpful when subdomains need to share information about a user.
+Therefore, specifying Domain is _less_ restrictive than omitting it. However, it can be helpful when subdomains need to share information about a user.
For example, if you set `Domain=mozilla.org`, cookies are available on subdomains like `developer.mozilla.org`.
@@ -81,7 +86,7 @@ The SameSite attribute lets servers specify whether/when cookies are sent with c
There are three different settings for the SameSite attribute:
* **Strict** - The cookie is only sent to the site where it originated.
-* **Lax** - Similar to "Strict" except that cookies are sent when the user navigates to the cookie's origin site.
+* **Lax** - Similar to "Strict" except that cookies are sent when the user navigates to the cookie's origin site.
* E.g. Following a link from an external site.
* **None** - specifies that cookies are sent on both originating and cross-site requests, but only in secure contexts (i.e., if SameSite=None then the Secure attribute must also be set).
@@ -115,7 +120,7 @@ If a cookie name has this prefix, it's accepted in a Set-Cookie header only if i
* It's marked with the Secure attribute
* It was sent from a secure origin.
-This is weaker than the __Host- prefix.
+This is weaker than the \_\_Host- prefix.
#### Strict Secure Cookies
@@ -159,7 +164,6 @@ The encryption you use can be a one-way lookup of the cookie value. It is possib
For instance, in the next section the Plug library gives you the ability to perform those actions within the `put_resp_cookie/4` function call. But if you store JSON Web Tokens (JWTs) as the value of your cookie, you can achieve similar signature results through the JWTs themselves.
-
### Resources
1. https://cloud.google.com/cdn/docs/using-signed-cookies#:~:text=Signed%20cookies%20give%20time%2Dlimited,t%20feasible%20in%20your%20application
@@ -173,7 +177,7 @@ In the Phoenix Framework, you would use functionality found within the [Plug lib
**Given the "Perfect Cookie" outlined above, how would you assign that cookie using the Plug library?**
-*Fill out the `put_resp_cookie/4` function arguments with the settings outlined in the previous section, no other code changes should be necessary.*
+_Fill out the `put_resp_cookie/4` function arguments with the settings outlined in the previous section, no other code changes should be necessary._
```elixir
cookie_name = "CHANGE_ME_TOO"
@@ -186,38 +190,41 @@ conn
# path: ,
# secure: ,
# http_only: ,
- # same_site:
+ # same_site:
)
```
-## Data Privacy For Cookies
+## Data Privacy For Cookies
-### Storing personal information
-While cookies by themselves can not dig and research your information, they do store personal information in at least 2 ways: form information and ad tracking.
+### Storing personal information
-Personal information is not generated by the cookies themselves, but are through user input via website registration pages, payments pages, and other online forms. To ensure proper security measures are in place this information should be encoded through limited interaction via SSL (secure socket layer) certified pages.
+While cookies by themselves can not dig and research your information, they do store personal information in at least 2 ways: form information and ad tracking.
-### Tracking User Behavior
+Personal information is not generated by the cookies themselves, but are through user input via website registration pages, payments pages, and other online forms. To ensure proper security measures are in place this information should be encoded through limited interaction via SSL (secure socket layer) certified pages.
-For systems that use third party ad serving networks, such as Google's AdSense / AdWord pose additional privacy concerns. When leveraging ad serving platforms there is an impact to user privacy being there is no obvious consent given for such tracking. With the rapid evolution around cookie based ad services and tracking user behavior, it brings up the privacy concern of using default standards for cookies.
+### Tracking User Behavior
+
+For systems that use third party ad serving networks, such as Google's AdSense / AdWord pose additional privacy concerns. When leveraging ad serving platforms there is an impact to user privacy being there is no obvious consent given for such tracking. With the rapid evolution around cookie based ad services and tracking user behavior, it brings up the privacy concern of using default standards for cookies.
#### Opt Out Cookies
+
Under an opt out scheme, consumers are notified via an alert or window when they load a website. The user must consent to the notice before they can navigate the site and any cookies are planted. At a minimum, the notice is to contain the following: disclosure of information gathering practices, the uses for this information, and policies for processing and disposing of this data.
Opt-out cookies are essentially cookies used to avoid cookies. When a website creates an opt-out cookie in your browser folder, it enables you to block that same website from installing future cookies.With this, Opt Out cookies offer safeguards for user information, and help secure systems against potential security concerns regarding “hidden” cookies
-#### Opt In Cookies
+#### Opt In Cookies
+
Opt-in is the process that describes an affirmative action user takes to offer their consent for companies to use their data. Unticked checkboxes or buttons are the most common way in which you can implement opt-in mechanisms to obtain users’ consent.
-#### Which One To Use?
+#### Which One To Use?
+
If you want to be legally compliant, it is safer to have both the options with opt-out as the default.
-
+
### Resources
1. https://allaboutcookies.org/privacy-issues-cookies
2. https://www.cookielawinfo.com/opt-in-vs-opt-out/
-
[**<- Previous Module: Elixir Security**](./5-elixir.livemd) || [**Next Module: Security Anti-Patterns ->**](./7-anti-patterns.livemd)
diff --git a/modules/7-anti-patterns.livemd b/modules/7-anti-patterns.livemd
index 8bfef09..7de1d62 100644
--- a/modules/7-anti-patterns.livemd
+++ b/modules/7-anti-patterns.livemd
@@ -1,5 +1,13 @@
# ESCT: Part 7 - Security Anti-Patterns
+```elixir
+Mix.install([
+ {:grading_client, path: "#{__DIR__}/grading_client"}
+])
+
+:ok
+```
+
## Introduction
In Software Engineering, an Anti-Pattern typically is a common response to a recurring problem that is usually ineffective and risks being highly counterproductive.
@@ -25,7 +33,7 @@ Obscurity means keeping the underlying system's security loopholes a secret to a
Typically, a hacker's approach in exploiting a system begins with mapping the attack surface. If there is no public information on those weak areas, hackers will find the system more difficult to penetrate and will eventually delay or postpone its malicious objective.
-**Here's the secret**...hackers are really, *really* good at figuring out what you don't want them to. If you were to take an obfuscator to your code before deploying it to production, you would definitely throw a wrench in an attackers plan, but it just delays the inevitable. It is not a tangible, technical mechanism that cannot be undone (e.g. hashing algorithms).
+**Here's the secret**...hackers are really, _really_ good at figuring out what you don't want them to. If you were to take an obfuscator to your code before deploying it to production, you would definitely throw a wrench in an attackers plan, but it just delays the inevitable. It is not a tangible, technical mechanism that cannot be undone (e.g. hashing algorithms).
### Example
@@ -65,7 +73,7 @@ Penguin.slide([3, 4, 5, 2, 1])
**What sort method is the module above using?**
-*Uncomment the line with your answer.*
+_Uncomment the line with your answer._
```elixir
# answer = :bubble_sort
diff --git a/modules/8-cicd.livemd b/modules/8-cicd.livemd
index 0e4e7ef..ab6c798 100644
--- a/modules/8-cicd.livemd
+++ b/modules/8-cicd.livemd
@@ -1,16 +1,21 @@
# ESCT: Part 8 - CI/CD Tools
```elixir
-Mix.install([:sobelow])
+Mix.install([
+ {:grading_client, path: "#{__DIR__}/grading_client"},
+ :sobelow
+])
+
+:ok
```
## Introduction
-Just like there's more to making software than just writing code, there's more to *securing* software than just reviewing code.
+Just like there's more to making software than just writing code, there's more to _securing_ software than just reviewing code.
Part of the development lifecycle includes deploying code and it is here that we can institute automated tooling and tests to assist in the detection of insecurities and potentially prevent vulnerabilities from reach production whatsoever!
-This module will cover over some of the automated processes you may see in a CI/CD pipeline and how they work at a high level. Important to note is most of these tools can be run in a number of different ways - meaning they don't *have* to be run in the CI/CD pipeline and instead can be run locally.
+This module will cover over some of the automated processes you may see in a CI/CD pipeline and how they work at a high level. Important to note is most of these tools can be run in a number of different ways - meaning they don't _have_ to be run in the CI/CD pipeline and instead can be run locally.
## Table of Contents
diff --git a/modules/9-secure-road.livemd b/modules/9-secure-road.livemd
index 7090efd..7e51430 100644
--- a/modules/9-secure-road.livemd
+++ b/modules/9-secure-road.livemd
@@ -1,11 +1,18 @@
# ESCT: Part 9 - The Secure Road
+```elixir
+Mix.install([
+ {:grading_client, path: "#{__DIR__}/grading_client"}
+])
+
+:ok
+```
+
## Introduction
> Two roads diverged in a wood, and I,
> I took the one less traveled by,
-> And that has made all the difference.
-> \- Robert Frost, The Road Not Taken (1916)
+> And that has made all the difference.
> \- Robert Frost, The Road Not Taken (1916)
Wow! Look at us, it was a long journey to get here and major kudos to you for getting through it all. I bet you're killing those quiz questions!
@@ -15,9 +22,9 @@ Thinking about Robert Frost's poem above - choosing to build code in a secure ma
When we build code using insecure techniques (intentionally or unintentionally) then technical debt is created in the form of security debt; even if building insecure code took less time, you inveitably will have to fix the security issues at some point.
-Can you imagine if Robert Frost decided to take the road *more* traveled, walked all that way, just to find out the road is out a ways ahead and had to walk back just to go down the road less traveled? It's the same for building Secure Code from the get-go.
+Can you imagine if Robert Frost decided to take the road _more_ traveled, walked all that way, just to find out the road is out a ways ahead and had to walk back just to go down the road less traveled? It's the same for building Secure Code from the get-go.
-With all that in mind, in this final module let's explore some examples of Data Systems that you *could* build...but they may not be the *most* secure systems from the onset and how we can go about rearchitecting them to be better.
+With all that in mind, in this final module let's explore some examples of Data Systems that you _could_ build...but they may not be the _most_ secure systems from the onset and how we can go about rearchitecting them to be better.
## Table of Contents
@@ -36,7 +43,7 @@ A) Have a single, securely generated token that each of the sender services has
B) Each sender service has a unique securely generated token and the receiver stores copies of each of those to check against whenever a request comes through?
-The answer in this case is **B** - even though technically both solutions *could* work, having a single version of the token increases the likelihood and impact if said token were leaked / exposed. Additionally, it would make it much harder to rotate the token in all the various services without causing an outage.
+The answer in this case is **B** - even though technically both solutions _could_ work, having a single version of the token increases the likelihood and impact if said token were leaked / exposed. Additionally, it would make it much harder to rotate the token in all the various services without causing an outage.
In the event of a token exposure in situation B, you would simple have to rotate the single token pairing between the sender and the receiver with effecting or compromising other services.
diff --git a/modules/grading_client/.formatter.exs b/modules/grading_client/.formatter.exs
new file mode 100644
index 0000000..d2cda26
--- /dev/null
+++ b/modules/grading_client/.formatter.exs
@@ -0,0 +1,4 @@
+# Used by "mix format"
+[
+ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
+]
diff --git a/modules/grading_client/.gitignore b/modules/grading_client/.gitignore
new file mode 100644
index 0000000..caa9a0b
--- /dev/null
+++ b/modules/grading_client/.gitignore
@@ -0,0 +1,26 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where third-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Ignore package tarball (built via "mix hex.build").
+grading_client-*.tar
+
+# Temporary files, for example, from tests.
+/tmp/
diff --git a/modules/grading_client/README.md b/modules/grading_client/README.md
new file mode 100644
index 0000000..5f2a87f
--- /dev/null
+++ b/modules/grading_client/README.md
@@ -0,0 +1,21 @@
+# GradingClient
+
+Simple client that abstracts out the `grading_server` connection.
+
+## Installation
+
+If [available in Hex](https://hex.pm/docs/publish), the package can be installed
+by adding `grading_client` to your list of dependencies in `mix.exs`:
+
+```elixir
+def deps do
+ [
+ {:grading_client, "~> 0.1.0"}
+ ]
+end
+```
+
+Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
+and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
+be found at .
+
diff --git a/modules/grading_client/lib/grading_client.ex b/modules/grading_client/lib/grading_client.ex
new file mode 100644
index 0000000..c3de88d
--- /dev/null
+++ b/modules/grading_client/lib/grading_client.ex
@@ -0,0 +1,35 @@
+defmodule GradingClient do
+ @moduledoc """
+ Module used for checking answers to questions.
+ """
+
+ @doc """
+ Checks the answer to a question.
+ """
+
+ @spec check_answer(any() | String.t(), integer(), integer()) :: :ok | {:error, String.t()}
+ def check_answer(answer, module_id, question_id) when not is_binary(answer) do
+ check_answer(module_id, question_id, "#{inspect(answer)}")
+ end
+
+ def check_answer(answer, module_id, question_id) do
+ # TODO: Make this configurable
+ url = "http://localhost:4000/api/answers/check"
+
+ headers = [
+ {"Content-Type", "application/json"}
+ ]
+
+ json = Jason.encode!(%{module_id: module_id, question_id: question_id, answer: answer})
+
+ %{body: body, status_code: 200} = HTTPoison.post!(url, json, headers)
+
+ %{"correct" => is_correct, "help_text" => help_text} = Jason.decode!(body)
+
+ if is_correct do
+ :ok
+ else
+ {:error, help_text}
+ end
+ end
+end
diff --git a/modules/grading_client/mix.exs b/modules/grading_client/mix.exs
new file mode 100644
index 0000000..88f6bb5
--- /dev/null
+++ b/modules/grading_client/mix.exs
@@ -0,0 +1,28 @@
+defmodule GradingClient.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :grading_client,
+ version: "0.1.0",
+ elixir: "~> 1.14",
+ start_permanent: Mix.env() == :prod,
+ deps: deps()
+ ]
+ end
+
+ # Run "mix help compile.app" to learn about applications.
+ def application do
+ [
+ extra_applications: [:logger]
+ ]
+ end
+
+ # Run "mix help deps" to learn about dependencies.
+ defp deps do
+ [
+ {:httpoison, "~> 2.1"},
+ {:jason, "~> 1.4"}
+ ]
+ end
+end
diff --git a/modules/grading_client/mix.lock b/modules/grading_client/mix.lock
new file mode 100644
index 0000000..c8939aa
--- /dev/null
+++ b/modules/grading_client/mix.lock
@@ -0,0 +1,15 @@
+%{
+ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
+ "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
+ "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
+ "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
+ "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
+ "httpoison": {:hex, :httpoison, "2.1.0", "655fd9a7b0b95ee3e9a3b535cf7ac8e08ef5229bab187fa86ac4208b122d934b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fc455cb4306b43827def4f57299b2d5ac8ac331cb23f517e734a4b78210a160c"},
+ "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
+ "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
+ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
+ "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
+ "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
+ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
+ "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
+}
diff --git a/modules/grading_client/test/grading_client_test.exs b/modules/grading_client/test/grading_client_test.exs
new file mode 100644
index 0000000..759392e
--- /dev/null
+++ b/modules/grading_client/test/grading_client_test.exs
@@ -0,0 +1,4 @@
+defmodule GradingClientTest do
+ use ExUnit.Case
+ doctest GradingClient
+end
diff --git a/modules/grading_client/test/test_helper.exs b/modules/grading_client/test/test_helper.exs
new file mode 100644
index 0000000..869559e
--- /dev/null
+++ b/modules/grading_client/test/test_helper.exs
@@ -0,0 +1 @@
+ExUnit.start()