diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e43b0f9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.DS_Store
diff --git a/modules/2-owasp.livemd b/modules/2-owasp.livemd
index 9accc72..02fa5de 100644
--- a/modules/2-owasp.livemd
+++ b/modules/2-owasp.livemd
@@ -1,14 +1,12 @@
# ESCT: Part 2 - OWASP
```elixir
-Mix.install([
- {:grading_client, path: "#{__DIR__}/grading_client"},
- :bcrypt_elixir,
- :httpoison,
- {:absinthe, "~> 1.7.0"},
- {:phoenix, "~> 1.0"},
- {:plug, "~> 1.3.2"}
-])
+Mix.install(
+ [
+ {:grading_client, path: "#{__DIR__}/grading_client"}
+ ],
+ config_path: "#{__DIR__}/grading_client/config/config.exs"
+)
md5_hash = :crypto.hash(:md5, "users_password")
bcrypt_salted_hash = Bcrypt.hash_pwd_salt("users_password")
@@ -103,27 +101,55 @@ Notable CWEs included are CWE-259: Use of Hard-coded Password, CWE-327: Broken o
_Please uncomment the function call that you believe is correct._
+
+
```elixir
-defmodule PasswordCompare do
- def option_one(password, md5_hash) do
- case :crypto.hash(:md5, password) == md5_hash do
- true -> :entry_granted_op1
- false -> :entry_denied_op1
+result =
+ defmodule PasswordCompare do
+ def option_one(password, md5_hash) do
+ case :crypto.hash(:md5, password) == md5_hash do
+ true -> :entry_granted_op1
+ false -> :entry_denied_op1
+ end
end
- end
- def option_two(password, bcrypt_salted_hash) do
- case Bcrypt.verify_pass(password, bcrypt_salted_hash) do
- true -> :entry_granted_op2
- false -> :entry_denied_op2
+ def option_two(password, bcrypt_salted_hash) do
+ case Bcrypt.verify_pass(password, bcrypt_salted_hash) do
+ true -> :entry_granted_op2
+ false -> :entry_denied_op2
+ end
end
end
-end
-# DO NOT CHANGE CODE ABOVE THIS LINE =========================
+[module_id, question_id] =
+ "#OWASP:1\ndefmodule PasswordCompare do\n def option_one(password, md5_hash) do\n case :crypto.hash(:md5, password) == md5_hash do\n true -> :entry_granted_op1\n false -> :entry_denied_op1\n end\n end\n\n def option_two(password, bcrypt_salted_hash) do\n case Bcrypt.verify_pass(password, bcrypt_salted_hash) do\n true -> :entry_granted_op2\n false -> :entry_denied_op2\n end\n end\nend\n\n# DO NOT CHANGE CODE ABOVE THIS LINE =========================\n\n# PasswordCompare.option_one(\"users_password\", md5_hash)\n# PasswordCompare.option_two(\"users_password\", bcrypt_salted_hash)"
+ |> String.split("\n", parts: 2)
+ |> hd()
+ |> String.trim_leading("#")
+ |> String.split(":", parts: 2)
+
+module_id =
+ case %{"OWASP" => OWASP}[String.trim(module_id)] do
+ nil -> raise "invalid module id: #{module_id}"
+ module_id -> module_id
+ end
+
+question_id =
+ case Integer.parse(String.trim(question_id)) do
+ {id, ""} -> id
+ _ -> raise "invalid question id: #{question_id}"
+ end
+
+case GradingClient.check_answer(module_id, question_id, result) do
+ :correct ->
+ IO.puts([IO.ANSI.green(), "Correct!", IO.ANSI.reset()])
+
+ {:incorrect, help_text} when is_binary(help_text) ->
+ IO.puts([IO.ANSI.red(), "Incorrect: ", IO.ANSI.reset(), help_text])
-# PasswordCompare.option_one("users_password", md5_hash)
-# PasswordCompare.option_two("users_password", bcrypt_salted_hash)
+ _ ->
+ IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()])
+end
```
@@ -244,19 +270,57 @@ Notable CWE included is CWE-1104: Use of Unmaintained Third-Party Components
### QUIZ
-**Which of the outdated components currently installed is vulnerable?**
+**Which of the outdated components listed below is vulnerable?**
_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: Check the changelogs for each dependency._
+
+
```elixir
-# CHANGE ME
-vulnerable_dependency = :vulnerable_dependency
+result =
+ (
+ answer =
+ Kino.Input.select("Answer",
+ ecto: "Ecto v2.2.2",
+ nx: "Nx v0.5.0",
+ plug: "Plug v1.3.2"
+ )
+
+ Kino.render(answer)
+ Kino.Input.read(answer)
+ )
+
+[module_id, question_id] =
+ "#OWASP:2\nanswer = \n Kino.Input.select(\"Answer\", [\n {:ecto, \"Ecto v2.2.2\"},\n {:nx, \"Nx v0.5.0\"},\n {:plug, \"Plug v1.3.2\"}\n ])\n\nKino.render(answer)\n\nKino.Input.read(answer)"
+ |> String.split("\n", parts: 2)
+ |> hd()
+ |> String.trim_leading("#")
+ |> String.split(":", parts: 2)
+
+module_id =
+ case %{"OWASP" => OWASP}[String.trim(module_id)] do
+ nil -> raise "invalid module id: #{module_id}"
+ module_id -> module_id
+ end
+
+question_id =
+ case Integer.parse(String.trim(question_id)) do
+ {id, ""} -> id
+ _ -> raise "invalid question id: #{question_id}"
+ end
+
+case GradingClient.check_answer(module_id, question_id, result) do
+ :correct ->
+ IO.puts([IO.ANSI.green(), "Correct!", IO.ANSI.reset()])
-# DO NOT CHANGE CODE BELOW THIS LINE ============================
-Application.spec(vulnerable_dependency)[:vsn] |> List.to_string() |> IO.puts()
-IO.puts(vulnerable_dependency)
+ {:incorrect, help_text} when is_binary(help_text) ->
+ IO.puts([IO.ANSI.red(), "Incorrect: ", IO.ANSI.reset(), help_text])
+
+ _ ->
+ IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()])
+end
```
diff --git a/modules/grading_client/config/config.exs b/modules/grading_client/config/config.exs
new file mode 100644
index 0000000..becde76
--- /dev/null
+++ b/modules/grading_client/config/config.exs
@@ -0,0 +1 @@
+import Config
diff --git a/modules/grading_client/lib/grading_client.ex b/modules/grading_client/lib/grading_client.ex
index c3de88d..83efe42 100644
--- a/modules/grading_client/lib/grading_client.ex
+++ b/modules/grading_client/lib/grading_client.ex
@@ -7,29 +7,8 @@ defmodule GradingClient do
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
+ @spec check_answer(any(), any(), any()) :: :correct | {:incorrect, String.t() | nil}
+ def check_answer(module_id, question_id, answer) do
+ GradingClient.Answers.check(module_id, question_id, answer)
end
end
diff --git a/modules/grading_client/lib/grading_client/answer.ex b/modules/grading_client/lib/grading_client/answer.ex
new file mode 100644
index 0000000..57723cf
--- /dev/null
+++ b/modules/grading_client/lib/grading_client/answer.ex
@@ -0,0 +1,11 @@
+defmodule GradingClient.Answer do
+ @enforce_keys [:question_id, :module_id, :answer, :help_text]
+ defstruct [:question_id, :module_id, :answer, :help_text]
+
+ @type t :: %__MODULE__{
+ module_id: term(),
+ question_id: term(),
+ answer: term(),
+ help_text: term()
+ }
+end
diff --git a/modules/grading_client/lib/grading_client/answers.ex b/modules/grading_client/lib/grading_client/answers.ex
new file mode 100644
index 0000000..af3e9d2
--- /dev/null
+++ b/modules/grading_client/lib/grading_client/answers.ex
@@ -0,0 +1,72 @@
+defmodule GradingClient.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.
+ """
+ use GenServer
+
+ alias GradingClient.Answer
+
+ @table :answer_store
+
+ def start_link(opts) do
+ GenServer.start_link(__MODULE__, opts, name: __MODULE__)
+ end
+
+ @impl true
+ def init(opts) do
+ :ets.new(@table, [:set, :named_table])
+
+ filename = opts[:filename]
+
+ {answers, _} = Code.eval_file(filename)
+
+ modules =
+ MapSet.new(answers, fn answer ->
+ :ets.insert(@table, {{answer.module_id, answer.question_id}, answer})
+ answer.module_id
+ end)
+
+ {:ok, %{modules: modules}}
+ end
+
+ @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
+ GenServer.call(__MODULE__, {:check, module_id, question_id, answer})
+ end
+
+ @doc """
+ Returns the list of modules.
+ """
+ @spec get_modules() :: [atom()]
+ def get_modules() do
+ GenServer.call(__MODULE__, :get_modules)
+ end
+
+ @impl true
+ def handle_call(:get_modules, _from, state) do
+ {:reply, state.modules, state}
+ end
+
+ @impl true
+ def handle_call({:check, module_id, question_id, answer}, _from, state) do
+ result =
+ case :ets.lookup(@table, {module_id, question_id}) do
+ [] ->
+ {:incorrect, "Question not found"}
+
+ [{_id, %Answer{answer: correct_answer, help_text: help_text}}] ->
+
+ if answer == correct_answer do
+ :correct
+ else
+ {:incorrect, help_text}
+ end
+ end
+
+ {:reply, result, state}
+ end
+end
diff --git a/modules/grading_client/lib/grading_client/application.ex b/modules/grading_client/lib/grading_client/application.ex
new file mode 100644
index 0000000..c7567ba
--- /dev/null
+++ b/modules/grading_client/lib/grading_client/application.ex
@@ -0,0 +1,17 @@
+defmodule GradingClient.Application do
+ use Application
+
+ def start(_type, _args) do
+ Kino.SmartCell.register(GradingClient.GradedCell)
+
+ default_filename = Path.join(:code.priv_dir(:grading_client), "answers.exs")
+
+ children = [
+ {GradingClient.Answers,
+ filename: Application.get_env(:grading_client, :answers_file, default_filename)}
+ ]
+
+ opts = [strategy: :one_for_one, name: GradingClient.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+end
diff --git a/modules/grading_client/lib/grading_client/graded_cell.ex b/modules/grading_client/lib/grading_client/graded_cell.ex
new file mode 100644
index 0000000..d4e7355
--- /dev/null
+++ b/modules/grading_client/lib/grading_client/graded_cell.ex
@@ -0,0 +1,109 @@
+defmodule GradingClient.GradedCell do
+ use Kino.JS
+ use Kino.JS.Live
+ use Kino.SmartCell, name: "Graded Cell"
+
+ @impl true
+ def init(attrs, ctx) do
+ source = attrs["source"] || ""
+
+ {:ok, assign(ctx, source: source), editor: [source: source, language: "elixir"]}
+ end
+
+ @impl true
+ def handle_editor_change(source, ctx) do
+ {:ok, assign(ctx, source: source)}
+ end
+
+ @impl true
+ def to_attrs(ctx) do
+ %{"source" => ctx.assigns.source}
+ end
+
+ @impl true
+ def to_source(attrs) do
+ modules = Map.new(GradingClient.Answers.get_modules(), &{inspect(&1), &1})
+
+ source_ast =
+ try do
+ source_attr = attrs["source"]
+ source = Code.string_to_quoted!(source_attr)
+
+ quote do
+ result = unquote(source)
+
+ [module_id, question_id] =
+ unquote(source_attr)
+ |> String.split("\n", parts: 2)
+ |> hd()
+ |> String.trim_leading("#")
+ |> String.split(":", parts: 2)
+
+ module_id =
+ case unquote(Macro.escape(modules))[String.trim(module_id)] do
+ nil ->
+ raise "invalid module id: #{module_id}"
+
+ module_id ->
+ module_id
+ end
+
+ question_id =
+ case Integer.parse(String.trim(question_id)) do
+ {id, ""} ->
+ id
+
+ _ ->
+ raise "invalid question id: #{question_id}"
+ end
+
+ case GradingClient.check_answer(module_id, question_id, result) do
+ :correct ->
+ IO.puts([IO.ANSI.green(), "Correct!", IO.ANSI.reset()])
+
+ {:incorrect, help_text} when is_binary(help_text) ->
+ IO.puts([IO.ANSI.red(), "Incorrect: ", IO.ANSI.reset(), help_text])
+
+ _ ->
+ IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()])
+ end
+ end
+ rescue
+ error ->
+ IO.inspect(error)
+ {:<<>>, [delimiter: ~s["""]], [attrs["source"] <> "\n"]}
+ end
+
+ Kino.SmartCell.quoted_to_string(source_ast)
+ end
+
+ @impl true
+ def handle_connect(ctx) do
+ {:ok, %{}, ctx}
+ end
+
+ asset "main.js" do
+ """
+ export function init(ctx, payload) {
+ ctx.importCSS("main.css");
+
+ root.innerHTML = `
+
+ `;
+ }
+ """
+ end
+
+ asset "main.css" do
+ """
+ .app {
+ padding: 8px 16px;
+ border: solid 1px #cad5e0;
+ border-radius: 0.5rem 0.5rem 0 0;
+ border-bottom: none;
+ }
+ """
+ end
+end
diff --git a/modules/grading_client/mix.exs b/modules/grading_client/mix.exs
index 88f6bb5..b658b86 100644
--- a/modules/grading_client/mix.exs
+++ b/modules/grading_client/mix.exs
@@ -14,6 +14,7 @@ defmodule GradingClient.MixProject do
# Run "mix help compile.app" to learn about applications.
def application do
[
+ mod: {GradingClient.Application, []},
extra_applications: [:logger]
]
end
@@ -21,8 +22,9 @@ defmodule GradingClient.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
- {:httpoison, "~> 2.1"},
- {:jason, "~> 1.4"}
+ {:kino, "~> 0.10"},
+ {:bcrypt_elixir, "~> 3.2"},
+ {:httpoison, "~> 2.2"}
]
end
end
diff --git a/modules/grading_client/mix.lock b/modules/grading_client/mix.lock
index c8939aa..36afecb 100644
--- a/modules/grading_client/mix.lock
+++ b/modules/grading_client/mix.lock
@@ -1,15 +1,44 @@
%{
+ "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.1", "e361261a0401d82dadc1ab7b969f91d250bf7577283e933fe8c5b72f8f5b3c46", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "81170177d5c2e280d12141a0b9d9e299bf731535e2d959982bdcd4cfe3c82865"},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
- "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
+ "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
+ "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
+ "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
+ "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
+ "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.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"},
"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"},
+ "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"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"},
+ "fss": {:hex, :fss, "0.1.1", "9db2344dbbb5d555ce442ac7c2f82dd975b605b50d169314a20f08ed21e08642", [:mix], [], "hexpm", "78ad5955c7919c3764065b21144913df7515d52e228c09427a004afe9c1a16b0"},
+ "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.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.4.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", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"},
+ "httpoison": {:hex, :httpoison, "2.2.2", "15420e9e5bbb505b931b2f589dc8be0c3b21e2a91a2c6ba882d99bf8f3ad499d", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "de7ac49fe2ffd89219972fdf39b268582f6f7f68d8cd29b4482dacca1ce82324"},
"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"},
+ "kino": {:hex, :kino, "0.15.3", "c99e21fc3e5d89513120295b91efc3efd18f7c1fb83875edced9d06ada13a2c0", [:mix], [{:fss, "~> 0.1.0", [hex: :fss, repo: "hexpm", optional: false]}, {:nx, "~> 0.1", [hex: :nx, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:table, "~> 0.1.2", [hex: :table, repo: "hexpm", optional: false]}], "hexpm", "11f62457ce6ac97ad377db9fcde168361fcf0de7db2a47b6f570607dc7897753"},
"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"},
+ "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
+ "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
+ "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
+ "phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [: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", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"},
+ "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [: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]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.9", "4dc5e535832733df68df22f9de168b11c0c74bca65b27b088a10ac36dfb75d04", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {: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.3 or ~> 4.0", [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]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1dccb04ec8544340e01608e108f32724458d0ac4b07e551406b3b920c40ba2e5"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
+ "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
+ "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"},
+ "plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [: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", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"},
+ "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
+ "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
+ "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.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
+ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
+ "table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"},
+ "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
+ "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
+ "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
+ "yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"},
}
diff --git a/modules/grading_client/priv/answers.exs b/modules/grading_client/priv/answers.exs
new file mode 100644
index 0000000..bcd5eb8
--- /dev/null
+++ b/modules/grading_client/priv/answers.exs
@@ -0,0 +1,29 @@
+alias GradingClient.Answer
+
+to_answers = fn module_name, answers ->
+ Enum.map(answers, fn data ->
+ %Answer{
+ module_id: module_name,
+ question_id: data.question_id,
+ answer: data.answer,
+ help_text: data[:help_text]
+ }
+ end)
+end
+
+owasp_questions = [
+ %{
+ question_id: 1,
+ answer: :entry_granted_op2,
+ help_text: "Research MD5 Rainbow Tables"
+ },
+ %{
+ question_id: 2,
+ answer: :plug,
+ help_text: "Check the changelog for the next minor or major release of each option."
+ }
+]
+
+List.flatten([
+ to_answers.(OWASP, owasp_questions)
+])
diff --git a/modules/grading_client/test/grading_client_test.exs b/modules/grading_client/test/grading_client_test.exs
deleted file mode 100644
index 759392e..0000000
--- a/modules/grading_client/test/grading_client_test.exs
+++ /dev/null
@@ -1,4 +0,0 @@
-defmodule GradingClientTest do
- use ExUnit.Case
- doctest GradingClient
-end