Skip to content

feat: Add graded cells for all quiz cells #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions modules/3-ssdlc.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Mix.install([
{:grading_client, path: "#{__DIR__}/grading_client"}
])

System.put_env("envar_secret", "some-secret-password")

:ok
```

Expand Down Expand Up @@ -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"
<!-- livebook:{"attrs":"eyJzb3VyY2UiOiIjIFNETEM6MVxuc3VwZXJfc2VjcmV0X3Bhc3N3b3JkID0gXCJwQHNzdzByZFwiIn0","chunks":null,"kind":"Elixir.GradingClient.GradedCell","livebook_object":"smart_cell"} -->

# DO NOT CHANGE CODE BELOW THIS COMMENT
IO.puts(super_secret_password)
```elixir
result = super_secret_password = "p@ssw0rd"

[module_id, question_id] =
"# SDLC: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
Expand Down
144 changes: 87 additions & 57 deletions modules/4-graphql.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -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._
<!-- livebook:{"attrs":"eyJzb3VyY2UiOiIjIEdSQVBIUUw6MVxuXG5pbnB1dCA9IEtpbm8uSW5wdXQuc2VsZWN0KFwiQ2hvb3NlIHlvdXIgYW5zd2VyXCIsIFtcbiAgYTogXCJBUEk2XzIwMTlfTWFzc19Bc3NpZ25tZW50XCIsIFxuICBiOiBcIkFQSTEwXzIwMTlfSW5zdWZmaWNpZW50X0xvZ2dpbmdfTW9uaXRvcmluZ1wiLCBcbiAgYzogXCJBUEkzXzIwMTlfRXhjZXNzaXZlX0RhdGFfRXhwb3N1cmVcIixcbiAgZDogXCJBUEk0XzIwMTlfTGFja19vZl9SZXNvdXJjZXNfUmF0ZV9MaW1pdGluZ1wiXG5dKVxuXG5LaW5vLnJlbmRlcihpbnB1dClcblxuS2luby5JbnB1dC5yZWFkKGlucHV0KSJ9","chunks":null,"kind":"Elixir.GradingClient.GradedCell","livebook_object":"smart_cell"} -->

```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
Expand All @@ -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_
<!-- livebook:{"attrs":"eyJzb3VyY2UiOiIjIEdSQVBIUUw6MlxuXG4jIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbiMgT3B0aW9uIDFcbiNcbiMgSFRUUC8yIDQwMSBVbmF1dGhvcml6ZWRcbiMgRGF0ZTogVHVlcywgMTYgQXVnIDIwMjIgMjE6MDY6NDIgR01UXG4jIOKAplxuIyB7XG4jIFx04oCcZXJyb3LigJ064oCddG9rZW4gZXhwaXJlZOKAnVxuIyB7XG4jIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cbiMgT3B0aW9uIDJcbiNcbiMgSFRUUC8yIDIwMCBPS1xuIyBEYXRlOiBUdWVzLCAxNiBBdWcgMjAyMSAyMjowNjo0MiBHTVRcbiMg4oCmXG4jIHtcbiMgXHTigJxlcnJvcnPigJ06W1xuIyBcdFx0e1xuIyBcdFx0XHTigJxsb2NhdGlvbnPigJ06W1xuIyBcdFx0XHR7XG4jIFx0XHRcdFx04oCcY29sdW1u4oCdOjIsXG4jIFx0XHRcdFx0OmxpbmXigJ06MlxuIyBcdFx0XHR9XG4jIFx0XHRcdF0sXG4jIFx0XHRcdOKAnG1lc3NhZ2XigJ06IOKAnFBhcnNpbmcgZmFpbGVkIGF0XG4jIFx0XHR9XG4jIFx0XVxuIyB9XG4jIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG4jIE9wdGlvbiAzXG4jXG4jIEhUVFAvMiAyMDAgT0tcbiMgRGF0ZTogVHVlcywgMTYgQXVnIDIwMjIgMjE6MDY6NDIgR01UXG4jIOKAplxuIyB7XG4jIFx04oCcZXJyb3LigJ064oCdSUQgdG9rZW4gZm9yIHVzZXIgNTVlNGNiMDcgYXQgb3JnIDEyMzQgZXhwaXJlZOKAnVxuIyB7XG4jIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLVxuIyBPcHRpb24gNFxuI1xuIyBIVFRQLzIgNDA0IEZpbGUgTm90IEZvdW5kXG4jIERhdGU6IFR1ZXMsIDE2IEF1ZyAyMDIyIDIxOjA2OjQyIEdNVFxuIyDigKZcbiMge1xuIyBcdOKAnGVycm9y4oCdOuKAnS93d3cvaG9tZS9maWxlLnR4dCBub3QgZm91bmQg4oCdXG4jIHtcbiMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tXG5cbmlucHV0ID0gS2luby5JbnB1dC5zZWxlY3QoXCJDaG9vc2UgeW91ciBhbnN3ZXJcIiwgW1xuICBub25lOiBcIlwiLFxuICBhOiBcIk9wdGlvbiAxXCIsIFxuICBiOiBcIk9wdGlvbiAyXCIsIFxuICBjOiBcIk9wdGlvbiAzXCIsXG4gIGQ6IFwiT3B0aW9uIDRcIlxuXSwgZGVmYXVsdDogOm5vbmUpXG5cbktpbm8ucmVuZGVyKGlucHV0KVxuXG5LaW5vLklucHV0LnJlYWQoaW5wdXQpIn0","chunks":null,"kind":"Elixir.GradingClient.GradedCell","livebook_object":"smart_cell"} -->

```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)"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason you're doing it as one line and not a heredoc?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This style will be harder to edit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just how the smart cell saved the code. There might be a way to force the heredoc though

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see if there's a way to clean this raw code up.

I was only editing through the smart cell itself, so I didn't realize how unreadable the rendered code is

|> 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
Expand Down
Loading