Skip to content

Commit 615d76a

Browse files
authored
Merge pull request #84 from erlef/pv-feat/self-sufficient-grading-client
2 parents 8894ff6 + b38f4a3 commit 615d76a

File tree

12 files changed

+375
-65
lines changed

12 files changed

+375
-65
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.DS_Store

modules/2-owasp.livemd

Lines changed: 93 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
# ESCT: Part 2 - OWASP
22

33
```elixir
4-
Mix.install([
5-
{:grading_client, path: "#{__DIR__}/grading_client"},
6-
:bcrypt_elixir,
7-
:httpoison,
8-
{:absinthe, "~> 1.7.0"},
9-
{:phoenix, "~> 1.0"},
10-
{:plug, "~> 1.3.2"}
11-
])
4+
Mix.install(
5+
[
6+
{:grading_client, path: "#{__DIR__}/grading_client"}
7+
],
8+
config_path: "#{__DIR__}/grading_client/config/config.exs"
9+
)
1210

1311
md5_hash = :crypto.hash(:md5, "users_password")
1412
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
103101

104102
_Please uncomment the function call that you believe is correct._
105103

104+
<!-- livebook:{"attrs":"eyJtb2R1bGVfaWQiOm51bGwsInF1ZXN0aW9uX2lkIjpudWxsLCJzb3VyY2UiOiIjT1dBU1A6MVxuZGVmbW9kdWxlIFBhc3N3b3JkQ29tcGFyZSBkb1xuICBkZWYgb3B0aW9uX29uZShwYXNzd29yZCwgbWQ1X2hhc2gpIGRvXG4gICAgY2FzZSA6Y3J5cHRvLmhhc2goOm1kNSwgcGFzc3dvcmQpID09IG1kNV9oYXNoIGRvXG4gICAgICB0cnVlIC0+IDplbnRyeV9ncmFudGVkX29wMVxuICAgICAgZmFsc2UgLT4gOmVudHJ5X2RlbmllZF9vcDFcbiAgICBlbmRcbiAgZW5kXG5cbiAgZGVmIG9wdGlvbl90d28ocGFzc3dvcmQsIGJjcnlwdF9zYWx0ZWRfaGFzaCkgZG9cbiAgICBjYXNlIEJjcnlwdC52ZXJpZnlfcGFzcyhwYXNzd29yZCwgYmNyeXB0X3NhbHRlZF9oYXNoKSBkb1xuICAgICAgdHJ1ZSAtPiA6ZW50cnlfZ3JhbnRlZF9vcDJcbiAgICAgIGZhbHNlIC0+IDplbnRyeV9kZW5pZWRfb3AyXG4gICAgZW5kXG4gIGVuZFxuZW5kXG5cbiMgRE8gTk9UIENIQU5HRSBDT0RFIEFCT1ZFIFRISVMgTElORSA9PT09PT09PT09PT09PT09PT09PT09PT09XG5cbiMgUGFzc3dvcmRDb21wYXJlLm9wdGlvbl9vbmUoXCJ1c2Vyc19wYXNzd29yZFwiLCBtZDVfaGFzaClcbiMgUGFzc3dvcmRDb21wYXJlLm9wdGlvbl90d28oXCJ1c2Vyc19wYXNzd29yZFwiLCBiY3J5cHRfc2FsdGVkX2hhc2gpIn0","chunks":null,"kind":"Elixir.GradingClient.GradedCell","livebook_object":"smart_cell"} -->
105+
106106
```elixir
107-
defmodule PasswordCompare do
108-
def option_one(password, md5_hash) do
109-
case :crypto.hash(:md5, password) == md5_hash do
110-
true -> :entry_granted_op1
111-
false -> :entry_denied_op1
107+
result =
108+
defmodule PasswordCompare do
109+
def option_one(password, md5_hash) do
110+
case :crypto.hash(:md5, password) == md5_hash do
111+
true -> :entry_granted_op1
112+
false -> :entry_denied_op1
113+
end
112114
end
113-
end
114115

115-
def option_two(password, bcrypt_salted_hash) do
116-
case Bcrypt.verify_pass(password, bcrypt_salted_hash) do
117-
true -> :entry_granted_op2
118-
false -> :entry_denied_op2
116+
def option_two(password, bcrypt_salted_hash) do
117+
case Bcrypt.verify_pass(password, bcrypt_salted_hash) do
118+
true -> :entry_granted_op2
119+
false -> :entry_denied_op2
120+
end
119121
end
120122
end
121-
end
122123

123-
# DO NOT CHANGE CODE ABOVE THIS LINE =========================
124+
[module_id, question_id] =
125+
"#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)"
126+
|> String.split("\n", parts: 2)
127+
|> hd()
128+
|> String.trim_leading("#")
129+
|> String.split(":", parts: 2)
130+
131+
module_id =
132+
case %{"OWASP" => OWASP}[String.trim(module_id)] do
133+
nil -> raise "invalid module id: #{module_id}"
134+
module_id -> module_id
135+
end
136+
137+
question_id =
138+
case Integer.parse(String.trim(question_id)) do
139+
{id, ""} -> id
140+
_ -> raise "invalid question id: #{question_id}"
141+
end
142+
143+
case GradingClient.check_answer(module_id, question_id, result) do
144+
:correct ->
145+
IO.puts([IO.ANSI.green(), "Correct!", IO.ANSI.reset()])
146+
147+
{:incorrect, help_text} when is_binary(help_text) ->
148+
IO.puts([IO.ANSI.red(), "Incorrect: ", IO.ANSI.reset(), help_text])
124149

125-
# PasswordCompare.option_one("users_password", md5_hash)
126-
# PasswordCompare.option_two("users_password", bcrypt_salted_hash)
150+
_ ->
151+
IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()])
152+
end
127153
```
128154

129155
<!-- livebook:{"branch_parent_index":3} -->
@@ -244,19 +270,57 @@ Notable CWE included is CWE-1104: Use of Unmaintained Third-Party Components
244270

245271
### <span style="color:red">QUIZ</span>
246272

247-
**Which of the outdated components currently installed is vulnerable?**
273+
**Which of the outdated components listed below is vulnerable?**
248274

249275
_Please change the atom below to the name of the vulnerable package installed in this Livebook AND update the afflicted package._
250276

251-
_HINT: Installed dependencies can be found at the very top, it was the very first cell you ran._
277+
_HINT: Check the changelogs for each dependency._
278+
279+
<!-- livebook:{"attrs":"eyJtb2R1bGVfaWQiOm51bGwsInF1ZXN0aW9uX2lkIjpudWxsLCJzb3VyY2UiOiIjT1dBU1A6MlxuYW5zd2VyID0gXG4gIEtpbm8uSW5wdXQuc2VsZWN0KFwiQW5zd2VyXCIsIFtcbiAgICB7OmVjdG8sIFwiRWN0byB2Mi4yLjJcIn0sXG4gICAgezpueCwgXCJOeCB2MC41LjBcIn0sXG4gICAgezpwbHVnLCBcIlBsdWcgdjEuMy4yXCJ9XG4gIF0pXG5cbktpbm8ucmVuZGVyKGFuc3dlcilcblxuS2luby5JbnB1dC5yZWFkKGFuc3dlcikifQ","chunks":null,"kind":"Elixir.GradingClient.GradedCell","livebook_object":"smart_cell"} -->
252280

253281
```elixir
254-
# CHANGE ME
255-
vulnerable_dependency = :vulnerable_dependency
282+
result =
283+
(
284+
answer =
285+
Kino.Input.select("Answer",
286+
ecto: "Ecto v2.2.2",
287+
nx: "Nx v0.5.0",
288+
plug: "Plug v1.3.2"
289+
)
290+
291+
Kino.render(answer)
292+
Kino.Input.read(answer)
293+
)
294+
295+
[module_id, question_id] =
296+
"#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)"
297+
|> String.split("\n", parts: 2)
298+
|> hd()
299+
|> String.trim_leading("#")
300+
|> String.split(":", parts: 2)
301+
302+
module_id =
303+
case %{"OWASP" => OWASP}[String.trim(module_id)] do
304+
nil -> raise "invalid module id: #{module_id}"
305+
module_id -> module_id
306+
end
307+
308+
question_id =
309+
case Integer.parse(String.trim(question_id)) do
310+
{id, ""} -> id
311+
_ -> raise "invalid question id: #{question_id}"
312+
end
313+
314+
case GradingClient.check_answer(module_id, question_id, result) do
315+
:correct ->
316+
IO.puts([IO.ANSI.green(), "Correct!", IO.ANSI.reset()])
256317

257-
# DO NOT CHANGE CODE BELOW THIS LINE ============================
258-
Application.spec(vulnerable_dependency)[:vsn] |> List.to_string() |> IO.puts()
259-
IO.puts(vulnerable_dependency)
318+
{:incorrect, help_text} when is_binary(help_text) ->
319+
IO.puts([IO.ANSI.red(), "Incorrect: ", IO.ANSI.reset(), help_text])
320+
321+
_ ->
322+
IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()])
323+
end
260324
```
261325

262326
<!-- livebook:{"branch_parent_index":3} -->
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import Config

modules/grading_client/lib/grading_client.ex

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,8 @@ defmodule GradingClient do
77
Checks the answer to a question.
88
"""
99

10-
@spec check_answer(any() | String.t(), integer(), integer()) :: :ok | {:error, String.t()}
11-
def check_answer(answer, module_id, question_id) when not is_binary(answer) do
12-
check_answer(module_id, question_id, "#{inspect(answer)}")
13-
end
14-
15-
def check_answer(answer, module_id, question_id) do
16-
# TODO: Make this configurable
17-
url = "http://localhost:4000/api/answers/check"
18-
19-
headers = [
20-
{"Content-Type", "application/json"}
21-
]
22-
23-
json = Jason.encode!(%{module_id: module_id, question_id: question_id, answer: answer})
24-
25-
%{body: body, status_code: 200} = HTTPoison.post!(url, json, headers)
26-
27-
%{"correct" => is_correct, "help_text" => help_text} = Jason.decode!(body)
28-
29-
if is_correct do
30-
:ok
31-
else
32-
{:error, help_text}
33-
end
10+
@spec check_answer(any(), any(), any()) :: :correct | {:incorrect, String.t() | nil}
11+
def check_answer(module_id, question_id, answer) do
12+
GradingClient.Answers.check(module_id, question_id, answer)
3413
end
3514
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
defmodule GradingClient.Answer do
2+
@enforce_keys [:question_id, :module_id, :answer, :help_text]
3+
defstruct [:question_id, :module_id, :answer, :help_text]
4+
5+
@type t :: %__MODULE__{
6+
module_id: term(),
7+
question_id: term(),
8+
answer: term(),
9+
help_text: term()
10+
}
11+
end
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
defmodule GradingClient.Answers do
2+
@moduledoc """
3+
This module is responsible for checking if an answer is correct.
4+
It uses the `AnswerStore` to fetch the answer and compares if it is correct or not.
5+
"""
6+
use GenServer
7+
8+
alias GradingClient.Answer
9+
10+
@table :answer_store
11+
12+
def start_link(opts) do
13+
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
14+
end
15+
16+
@impl true
17+
def init(opts) do
18+
:ets.new(@table, [:set, :named_table])
19+
20+
filename = opts[:filename]
21+
22+
{answers, _} = Code.eval_file(filename)
23+
24+
modules =
25+
MapSet.new(answers, fn answer ->
26+
:ets.insert(@table, {{answer.module_id, answer.question_id}, answer})
27+
answer.module_id
28+
end)
29+
30+
{:ok, %{modules: modules}}
31+
end
32+
33+
@doc """
34+
Checks if the given answer is correct.
35+
"""
36+
@spec check(integer(), integer(), String.t()) :: :correct | {:incorrect, String.t()}
37+
def check(module_id, question_id, answer) do
38+
GenServer.call(__MODULE__, {:check, module_id, question_id, answer})
39+
end
40+
41+
@doc """
42+
Returns the list of modules.
43+
"""
44+
@spec get_modules() :: [atom()]
45+
def get_modules() do
46+
GenServer.call(__MODULE__, :get_modules)
47+
end
48+
49+
@impl true
50+
def handle_call(:get_modules, _from, state) do
51+
{:reply, state.modules, state}
52+
end
53+
54+
@impl true
55+
def handle_call({:check, module_id, question_id, answer}, _from, state) do
56+
result =
57+
case :ets.lookup(@table, {module_id, question_id}) do
58+
[] ->
59+
{:incorrect, "Question not found"}
60+
61+
[{_id, %Answer{answer: correct_answer, help_text: help_text}}] ->
62+
63+
if answer == correct_answer do
64+
:correct
65+
else
66+
{:incorrect, help_text}
67+
end
68+
end
69+
70+
{:reply, result, state}
71+
end
72+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
defmodule GradingClient.Application do
2+
use Application
3+
4+
def start(_type, _args) do
5+
Kino.SmartCell.register(GradingClient.GradedCell)
6+
7+
default_filename = Path.join(:code.priv_dir(:grading_client), "answers.exs")
8+
9+
children = [
10+
{GradingClient.Answers,
11+
filename: Application.get_env(:grading_client, :answers_file, default_filename)}
12+
]
13+
14+
opts = [strategy: :one_for_one, name: GradingClient.Supervisor]
15+
Supervisor.start_link(children, opts)
16+
end
17+
end

0 commit comments

Comments
 (0)