From 7e6ac009bb1863d1b43ae54b20b1cfadc62107b3 Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Mon, 14 Apr 2025 04:00:34 -0300 Subject: [PATCH 1/5] feat: chapter 3 quiz --- modules/3-ssdlc.livemd | 40 +++++++++++++++++++++---- modules/grading_client/priv/answers.exs | 11 ++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/modules/3-ssdlc.livemd b/modules/3-ssdlc.livemd index 77704c1..255b859 100644 --- a/modules/3-ssdlc.livemd +++ b/modules/3-ssdlc.livemd @@ -5,6 +5,8 @@ Mix.install([ {:grading_client, path: "#{__DIR__}/grading_client"} ]) +System.put_env("envar_secret", "some-secret-password") + :ok ``` @@ -45,12 +47,40 @@ A very easy way to prevent secrets being added to files is to access them via En _Use `System.get_env/1` on line 2._ -```elixir -# let's assume there is an environment variable named 'envar_secret' -super_secret_password = "p@ssw0rd" + -# DO NOT CHANGE CODE BELOW THIS COMMENT -IO.puts(super_secret_password) +```elixir +result = super_secret_password = "p@ssw0rd" + +[module_id, question_id] = + "# ESCT:1\nsuper_secret_password = \"p@ssw0rd\"" + |> String.split("\n", parts: 2) + |> hd() + |> String.trim_leading("#") + |> String.split(":", parts: 2) + +module_id = + case %{"ESCT" => ESCT, "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]) + + _ -> + IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()]) +end ``` ## Making Secret Rotation Easy diff --git a/modules/grading_client/priv/answers.exs b/modules/grading_client/priv/answers.exs index bcd5eb8..4fc832f 100644 --- a/modules/grading_client/priv/answers.exs +++ b/modules/grading_client/priv/answers.exs @@ -24,6 +24,15 @@ owasp_questions = [ } ] +esct_questions = [ + %{ + question_id: 1, + answer: "some-secret-password", + help_text: "Use System.get_env/1 to get the password from the environment variable." + } +] + List.flatten([ - to_answers.(OWASP, owasp_questions) + to_answers.(OWASP, owasp_questions), + to_answers.(ESCT, esct_questions) ]) From a3e87ccb91f49961d228dc9c77829fe8fa33eb5e Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Mon, 14 Apr 2025 04:18:08 -0300 Subject: [PATCH 2/5] feat: answers for chapter 4 --- modules/4-graphql.livemd | 144 ++++++++++++++---------- modules/grading_client/priv/answers.exs | 16 ++- 2 files changed, 102 insertions(+), 58 deletions(-) diff --git a/modules/4-graphql.livemd b/modules/4-graphql.livemd index b931942..f97b831 100644 --- a/modules/4-graphql.livemd +++ b/modules/4-graphql.livemd @@ -59,15 +59,52 @@ 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._ + ```elixir -# answer = :API6_2019_Mass_Assignment -# answer = :API10_2019_Insufficient_Logging_Monitoring -# answer = :API3_2019_Excessive_Data_Exposure -# answer = :API4_2019_Lack_of_Resources_Rate_Limiting - -IO.puts(answer) +result = + ( + input = + Kino.Input.select("Choose your answer", + a: "API6_2019_Mass_Assignment", + b: "API10_2019_Insufficient_Logging_Monitoring", + c: "API3_2019_Excessive_Data_Exposure", + d: "API4_2019_Lack_of_Resources_Rate_Limiting" + ) + + Kino.render(input) + Kino.Input.read(input) + ) + +[module_id, question_id] = + "# GRAPHQL:1\n\ninput = Kino.Input.select(\"Choose your answer\", [\n a: \"API6_2019_Mass_Assignment\", \n b: \"API10_2019_Insufficient_Logging_Monitoring\", \n c: \"API3_2019_Excessive_Data_Exposure\",\n d: \"API4_2019_Lack_of_Resources_Rate_Limiting\"\n])\n\nKino.render(input)\n\nKino.Input.read(input)" + |> String.split("\n", parts: 2) + |> hd() + |> String.trim_leading("#") + |> String.split(":", parts: 2) + +module_id = + case %{"ESCT" => ESCT, "GRAPHQL" => GRAPHQL, "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]) + + _ -> + IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()]) +end ``` ## Error Disclosure @@ -88,58 +125,51 @@ 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_ + ```elixir -# ------------------------------------------------------------- -# answer = 1 -# -# 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 -# } -# ] -# } -# -------------------------------------------------------------- -# 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” -# { -# --------------------------------------------------------------- -# answer = 4 -# -# 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) +result = + ( + input = + Kino.Input.select( + "Choose your answer", + [none: "", a: "Option 1", b: "Option 2", c: "Option 3", d: "Option 4"], + default: :none + ) + + Kino.render(input) + Kino.Input.read(input) + ) + +[module_id, question_id] = + "# GRAPHQL:2\n\n# -------------------------------------------------------------\n# Option 1\n#\n# HTTP/2 401 Unauthorized\n# Date: Tues, 16 Aug 2022 21:06:42 GMT\n# …\n# {\n# \t“error”:”token expired”\n# {\n# -------------------------------------------------------------\n# Option 2\n#\n# HTTP/2 200 OK\n# Date: Tues, 16 Aug 2021 22:06:42 GMT\n# …\n# {\n# \t“errors”:[\n# \t\t{\n# \t\t\t“locations”:[\n# \t\t\t{\n# \t\t\t\t“column”:2,\n# \t\t\t\t:line”:2\n# \t\t\t}\n# \t\t\t],\n# \t\t\t“message”: “Parsing failed at\n# \t\t}\n# \t]\n# }\n# --------------------------------------------------------------\n# Option 3\n#\n# HTTP/2 200 OK\n# Date: Tues, 16 Aug 2022 21:06:42 GMT\n# …\n# {\n# \t“error”:”ID token for user 55e4cb07 at org 1234 expired”\n# {\n# ---------------------------------------------------------------\n# Option 4\n#\n# HTTP/2 404 File Not Found\n# Date: Tues, 16 Aug 2022 21:06:42 GMT\n# …\n# {\n# \t“error”:”/www/home/file.txt not found ”\n# {\n# ---------------------------------------------------------------\n\ninput = Kino.Input.select(\"Choose your answer\", [\n none: \"\",\n a: \"Option 1\", \n b: \"Option 2\", \n c: \"Option 3\",\n d: \"Option 4\"\n], default: :none)\n\nKino.render(input)\n\nKino.Input.read(input)" + |> String.split("\n", parts: 2) + |> hd() + |> String.trim_leading("#") + |> String.split(":", parts: 2) + +module_id = + case %{"ESCT" => ESCT, "GRAPHQL" => GRAPHQL, "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]) + + _ -> + IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()]) +end ``` ### Resources diff --git a/modules/grading_client/priv/answers.exs b/modules/grading_client/priv/answers.exs index 4fc832f..254cab2 100644 --- a/modules/grading_client/priv/answers.exs +++ b/modules/grading_client/priv/answers.exs @@ -32,7 +32,21 @@ esct_questions = [ } ] +graphql_questions = [ + %{ + question_id: 1, + answer: :c, + help_text: "Read the first paragraph of this livebook again!" + }, + %{ + question_id: 2, + answer: :a, + help_text: "Read the first paragraph of this livebook again!" + } +] + List.flatten([ to_answers.(OWASP, owasp_questions), - to_answers.(ESCT, esct_questions) + to_answers.(ESCT, esct_questions), + to_answers.(GRAPHQL, graphql_questions) ]) From b43913cd4e8bbce82d293168caec3335267d923d Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Mon, 14 Apr 2025 04:19:12 -0300 Subject: [PATCH 3/5] fix: part3 naming --- modules/3-ssdlc.livemd | 4 ++-- modules/grading_client/priv/answers.exs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/3-ssdlc.livemd b/modules/3-ssdlc.livemd index 255b859..94dd24f 100644 --- a/modules/3-ssdlc.livemd +++ b/modules/3-ssdlc.livemd @@ -47,13 +47,13 @@ A very easy way to prevent secrets being added to files is to access them via En _Use `System.get_env/1` on line 2._ - + ```elixir result = super_secret_password = "p@ssw0rd" [module_id, question_id] = - "# ESCT:1\nsuper_secret_password = \"p@ssw0rd\"" + "# SDLC:1\nsuper_secret_password = \"p@ssw0rd\"" |> String.split("\n", parts: 2) |> hd() |> String.trim_leading("#") diff --git a/modules/grading_client/priv/answers.exs b/modules/grading_client/priv/answers.exs index 254cab2..9b5ffe9 100644 --- a/modules/grading_client/priv/answers.exs +++ b/modules/grading_client/priv/answers.exs @@ -24,7 +24,7 @@ owasp_questions = [ } ] -esct_questions = [ +sdlc_questions = [ %{ question_id: 1, answer: "some-secret-password", @@ -47,6 +47,6 @@ graphql_questions = [ List.flatten([ to_answers.(OWASP, owasp_questions), - to_answers.(ESCT, esct_questions), + to_answers.(SDLC, part3_questions), to_answers.(GRAPHQL, graphql_questions) ]) From 3be647b78f9dba5b2b75bcd6f7510dffe670c879 Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Mon, 14 Apr 2025 04:36:31 -0300 Subject: [PATCH 4/5] feat: chapter 5 --- modules/5-elixir.livemd | 168 ++++++++++++++++++++---- modules/grading_client/priv/answers.exs | 29 +++- 2 files changed, 168 insertions(+), 29 deletions(-) diff --git a/modules/5-elixir.livemd b/modules/5-elixir.livemd index ddc6ecf..b507a8b 100644 --- a/modules/5-elixir.livemd +++ b/modules/5-elixir.livemd @@ -5,7 +5,6 @@ Mix.install([ {:grading_client, path: "#{__DIR__}/grading_client"}, {:benchwarmer, github: "mroth/benchwarmer", ref: "12b5a96b38cef09f2bd49e5c2dd5024100c1e8af"}, :uuid, - :kino, :plug ]) ``` @@ -51,18 +50,53 @@ Beware of functions in applications/libraries that create atoms from input value _You should get a `true` result when you successfully fix the function._ + + ```elixir -malicious_user_input = UUID.uuid4() +result = + ( + malicious_user_input = UUID.uuid4() + + try do + malicious_user_input |> String.to_atom() + rescue + e -> e + end + ) + +[module_id, question_id] = + "# ELIXIR_SECURITY:1\nmalicious_user_input = UUID.uuid4()\n\ntry do\n malicious_user_input\n # ONLY CHANGE NEXT LINE\n |> String.to_atom()\nrescue\n e -> e\nend" + |> String.split("\n", parts: 2) + |> hd() + |> String.trim_leading("#") + |> String.split(":", parts: 2) + +module_id = + case %{ + "ELIXIR_SECURITY" => ELIXIR_SECURITY, + "GRAPHQL" => GRAPHQL, + "OWASP" => OWASP, + "SDLC" => SDLC + }[String.trim(module_id)] do + nil -> raise "invalid module id: #{module_id}" + module_id -> module_id + end -try do - malicious_user_input - # ONLY CHANGE NEXT LINE - |> String.to_atom() -rescue - _ -> - IO.puts("Are you protected against Atom Exhaustion?") - IO.puts("true") - nil +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 ``` @@ -203,25 +237,66 @@ The latter will raise a `BadBooleanError` when the function returns `:ok` or `{: _Uncomment the if statement that uses the correct boolean comparison._ + + ```elixir -defmodule SecurityCheck do - def validate(input, password_hash) do - case Plug.Crypto.secure_compare(input, password_hash) do - true -> :ok - false -> :access_denied +result = + ( + defmodule SecurityCheck do + def validate(input, password_hash) do + case Plug.Crypto.secure_compare(input, password_hash) do + true -> :ok + false -> :access_denied + end + end + + defexception message: "There was an issue" + end + + password = "some_secure_password_hash" + user_input = "some_string_which_obviously_isnt_the_same_as_the_password" + :ok + + try do + rescue + e -> e end + ) + +[module_id, question_id] = + "# ELIXIR_SECURITY:2\n\ndefmodule SecurityCheck do\n def validate(input, password_hash) do\n case Plug.Crypto.secure_compare(input, password_hash) do\n true -> :ok\n false -> :access_denied\n end\n end\n\n defexception message: \"There was an issue\"\nend\n\npassword = \"some_secure_password_hash\"\nuser_input = \"some_string_which_obviously_isnt_the_same_as_the_password\"\n:ok\n# DO NOT EDIT ANY CODE ABOVE THIS LINE =====================\n\ntry do\n# if SecurityCheck.validate(user_input, password) or raise(SecurityCheck) do :you_let_a_baddie_in end\n# if SecurityCheck.validate(user_input, password) || raise(SecurityCheck) do :you_let_a_baddie_in end\nrescue\n e -> e\nend" + |> String.split("\n", parts: 2) + |> hd() + |> String.trim_leading("#") + |> String.split(":", parts: 2) + +module_id = + case %{ + "ELIXIR_SECURITY" => ELIXIR_SECURITY, + "GRAPHQL" => GRAPHQL, + "OWASP" => OWASP, + "SDLC" => SDLC + }[String.trim(module_id)] do + nil -> raise "invalid module id: #{module_id}" + module_id -> module_id end - defexception message: "There was an issue" -end +question_id = + case Integer.parse(String.trim(question_id)) do + {id, ""} -> id + _ -> raise "invalid question id: #{question_id}" + end -password = "some_secure_password_hash" -user_input = "some_string_which_obviously_isnt_the_same_as_the_password" -:ok -# DO NOT EDIT ANY CODE ABOVE THIS LINE ===================== +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]) -# if SecurityCheck.validate(user_input, password) or raise(SecurityCheck) do :you_let_a_baddie_in end -# if SecurityCheck.validate(user_input, password) || raise(SecurityCheck) do :you_let_a_baddie_in end + _ -> + IO.puts([IO.ANSI.red(), "Incorrect.", IO.ANSI.reset()]) +end ``` ## Sensitive Data Exposure @@ -277,10 +352,49 @@ 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 -# ONLY EDIT THIS LINE -secret_table = :ets.new(:secret_table, [:public]) -:ets.info(secret_table)[:protection] +result = + ( + secret_table = :ets.new(:secret_table, [:public]) + :ets.info(secret_table)[:protection] + ) + +[module_id, question_id] = + "# ELIXIR_SECURITY:3\n\n# ONLY EDIT THIS LINE\nsecret_table = :ets.new(:secret_table, [:public])\n:ets.info(secret_table)[:protection]" + |> String.split("\n", parts: 2) + |> hd() + |> String.trim_leading("#") + |> String.split(":", parts: 2) + +module_id = + case %{ + "ELIXIR_SECURITY" => ELIXIR_SECURITY, + "GRAPHQL" => GRAPHQL, + "OWASP" => OWASP, + "SDLC" => SDLC + }[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 ``` ## Resource diff --git a/modules/grading_client/priv/answers.exs b/modules/grading_client/priv/answers.exs index 9b5ffe9..3778750 100644 --- a/modules/grading_client/priv/answers.exs +++ b/modules/grading_client/priv/answers.exs @@ -45,8 +45,33 @@ graphql_questions = [ } ] +elixir_security_questions = [ + %{ + question_id: 1, + answer: %ArgumentError{ + message: + "errors were found at the given arguments:\n\n * 1st argument: not an already existing atom\n" + }, + help_text: "Read the Prevention section above." + }, + %{ + question_id: 2, + answer: %BadBooleanError{ + term: :access_denied, + operator: :or + }, + help_text: "Read the Prevention section above." + }, + %{ + question_id: 3, + answer: :private, + help_text: "Read the documentation for :ets.new/2" + } +] + List.flatten([ to_answers.(OWASP, owasp_questions), - to_answers.(SDLC, part3_questions), - to_answers.(GRAPHQL, graphql_questions) + to_answers.(SDLC, sdlc_questions), + to_answers.(GRAPHQL, graphql_questions), + to_answers.(ELIXIR_SECURITY, elixir_security_questions) ]) From d3b694cf3b73037784361f5ca1ef7d3e7a5a3bd7 Mon Sep 17 00:00:00 2001 From: Paulo Valente <16843419+polvalente@users.noreply.github.com> Date: Mon, 14 Apr 2025 04:58:32 -0300 Subject: [PATCH 5/5] feat: add missing answers --- modules/6-cookies.livemd | 62 ++++++++++++++++++++----- modules/7-anti-patterns.livemd | 56 ++++++++++++++++++++-- modules/grading_client/priv/answers.exs | 23 ++++++++- 3 files changed, 123 insertions(+), 18 deletions(-) diff --git a/modules/6-cookies.livemd b/modules/6-cookies.livemd index 7d53907..475d367 100644 --- a/modules/6-cookies.livemd +++ b/modules/6-cookies.livemd @@ -181,19 +181,57 @@ In the Phoenix Framework, you would use functionality found within the [Plug lib _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" - -conn -|> Plug.Conn.put_resp_cookie( - cookie_name, - <<42::16>> - # domain: , - # path: , - # secure: , - # http_only: , - # same_site: -) +result = + ( + cookie_name = "CHANGE_ME" + + cookie = + conn + |> Plug.Conn.fetch_cookies() + |> Plug.Conn.get_resp_cookies() + |> Map.fetch!(cookie_name) + + {cookie, binary_part(cookie_name, 0, 6)} + ) + +[module_id, question_id] = + "# COOKIE_SECURITY:1 \n\ncookie_name = \"CHANGE_ME\"\n\n# Uncomment and change the put_resp_cookie call below\n# conn =\n# Plug.Conn.put_resp_cookie(\n# conn,\n# cookie_name,\n# <<0::8, 42::8>>,\n# domain: ...,\n# path: ...,\n# secure: ...,\n# http_only: ...,\n# same_site: ...\n# )\n\ncookie = \n conn\n |> Plug.Conn.fetch_cookies()\n |> Plug.Conn.get_resp_cookies()\n |> Map.fetch!(cookie_name)\n\n{cookie, binary_part(cookie_name, 0, 6)}" + |> String.split("\n", parts: 2) + |> hd() + |> String.trim_leading("#") + |> String.split(":", parts: 2) + +module_id = + case %{ + "COOKIE_SECURITY" => COOKIE_SECURITY, + "ELIXIR_SECURITY" => ELIXIR_SECURITY, + "GRAPHQL" => GRAPHQL, + "OWASP" => OWASP, + "SDLC" => SDLC + }[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 ``` ## Data Privacy For Cookies diff --git a/modules/7-anti-patterns.livemd b/modules/7-anti-patterns.livemd index 3e46ba5..b5bc65e 100644 --- a/modules/7-anti-patterns.livemd +++ b/modules/7-anti-patterns.livemd @@ -75,13 +75,59 @@ Penguin.slide([3, 4, 5, 2, 1]) _Uncomment the line with your answer._ + + ```elixir -# answer = :bubble_sort -# answer = :merge_sort -# answer = :quick_sort -# answer = :random_sort +result = + ( + input = + Kino.Input.select("Answer", + a: "Bubble Sort", + b: "Merge Sort", + c: "Quick Sort", + d: "Random Sort" + ) + + Kino.render(input) + Kino.Input.read(input) + ) + +[module_id, question_id] = + "# ANTIPATTERNS:1\n\ninput = Kino.Input.select(\"Answer\", [\n a: \"Bubble Sort\",\n b: \"Merge Sort\",\n c: \"Quick Sort\",\n d: \"Random Sort\"\n])\n\nKino.render(input)\n\nKino.Input.read(input)" + |> String.split("\n", parts: 2) + |> hd() + |> String.trim_leading("#") + |> String.split(":", parts: 2) + +module_id = + case %{ + "ANTIPATTERNS" => ANTIPATTERNS, + "COOKIE_SECURITY" => COOKIE_SECURITY, + "ELIXIR_SECURITY" => ELIXIR_SECURITY, + "GRAPHQL" => GRAPHQL, + "OWASP" => OWASP, + "SDLC" => SDLC + }[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 -IO.puts(answer) +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 ``` ## Frontend Authorization Checks diff --git a/modules/grading_client/priv/answers.exs b/modules/grading_client/priv/answers.exs index 3778750..0ef6cf2 100644 --- a/modules/grading_client/priv/answers.exs +++ b/modules/grading_client/priv/answers.exs @@ -69,9 +69,30 @@ elixir_security_questions = [ } ] +cookie_security_questions = [ + %{ + question_id: 1, + answer: { + %{value: <<0, 42>>, path: "/", secure: true, http_only: true, same_site: "Strict"}, + "__Host" + }, + help_text: "Read the section about the __Host prefix." + } +] + +antipatterns_questions = [ + %{ + question_id: 1, + answer: :c, + help_text: "Look-up the pseudocode for each algorithm." + } +] + List.flatten([ to_answers.(OWASP, owasp_questions), to_answers.(SDLC, sdlc_questions), to_answers.(GRAPHQL, graphql_questions), - to_answers.(ELIXIR_SECURITY, elixir_security_questions) + to_answers.(ELIXIR_SECURITY, elixir_security_questions), + to_answers.(COOKIE_SECURITY, cookie_security_questions), + to_answers.(ANTIPATTERNS, antipatterns_questions) ])