Skip to content
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
44 changes: 37 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Structured reading for BSCP and Security+ (SY0-701 style): what to compare in a

<img alt="Study Desk: an open notebook, a teal ribbon, and a copper pen" src="docs/readme/hero.jpg" width="880" />

[Quick Start](#-quick-start) · [Screenshots](#-screenshots) · [How It Works](#-how-it-works) · [The Repo](#-whats-in-this-repo) · [Lesson Anatomy](#-anatomy-of-a-lesson) · [The Rules](#-non-negotiables) · [FAQ](#-faq--troubleshooting)
[Quick Start](#-quick-start) · [Screenshots](#-screenshots) · [How It Works](#-how-it-works) · [The Repo](#-whats-in-this-repo) · [Lesson Anatomy](#-anatomy-of-a-lesson) · [Quizzes](#-adding-a-quiz) · [The Rules](#-non-negotiables) · [FAQ](#-faq--troubleshooting)

</div>

Expand All @@ -40,6 +40,7 @@ Question practice stays in **Drill**, with the coach. This app is the reading ha
- [Quick start](#-quick-start)
- [What's in this repo](#-whats-in-this-repo)
- [Anatomy of a lesson](#-anatomy-of-a-lesson)
- [Adding a quiz](#-adding-a-quiz)
- [The two tracks](#-the-two-tracks)
- [Non-negotiables](#-non-negotiables)
- [FAQ / troubleshooting](#-faq--troubleshooting)
Expand All @@ -58,6 +59,10 @@ The banner above is an illustration. These are the running desk.
<img alt="Search dialog open over the notes" src="docs/readme/search.png" width="48%" />
</p>

<p align="center">
<img alt="Boundary checks quiz with the explanation shown" src="docs/readme/quiz.png" width="820" />
</p>

<p align="center">
<img alt="Mobile navigation menu" src="docs/readme/mobile.png" width="280" />
<img alt="Study Desk favicon, an open book on teal" src="public/favicon.png" width="128" />
Expand Down Expand Up @@ -136,16 +141,17 @@ cert-study/
├── src/content.config.ts 📚 Astro content collections for the MDX lessons
├── content/
│ ├── bscp/ 🔓 BSCP lessons (*.mdx)
│ └── security-plus/ 🛡️ Security+ lessons (*.mdx)
│ ├── security-plus/ 🛡️ Security+ lessons (*.mdx)
│ └── quizzes/ ✅ Concept checks (*.json)
├── astro.config.mjs 🏝️ Framework integrations
├── bunfig.toml 📦 Forces the binary bun.lockb lockfile
└── bun.lockb 🔒 Lockfile committed for a clean clone
```

| Path | What it is |
| --- | --- |
| **`src/pages/`** | Astro routes. Home, `/bscp`, `/security-plus`, `/[track]/[slug]`, `/search`, `/about`. |
| **`src/components/react/`** | Search dialog. |
| **`src/pages/`** | Astro routes. Home, `/bscp`, `/security-plus`, `/[track]/[slug]`, `/quizzes`, `/search`, `/about`. |
| **`src/components/react/`** | Search dialog, quiz player, and the in-browser quiz editor. |
| **`src/components/vue/`** | Track hub filters and module list. |
| **`src/components/svelte/`** | Lesson progress buttons. Opening a lesson marks it in progress. |
| **`src/components/solid/`** | Light / Dark control. |
Expand Down Expand Up @@ -229,6 +235,29 @@ Security+ notes use `<Exam>` when the point is how a stem is usually framed. Do

The MDX compiler keeps these components and strips other `{expressions}`. In the body, avoid raw `{` `}` and avoid `<` except on `Lab`, `Defend`, `Pitfall`, and `Exam`.

## 🧩 Adding a quiz

A quiz is one JSON file, or a quiz you save from `/quizzes/new`. The filename is the slug.

```json
{
"title": "Boundary checks",
"track": "bscp",
"summary": "At least twenty-four characters describing the check.",
"lesson": { "track": "bscp", "slug": "access-control" },
"questions": [
{
"prompt": "A full question, at least twelve characters.",
"choices": ["First distinct choice", "Second distinct choice"],
"answer": 0,
"explain": "Why that choice matches the note, in your own words."
}
]
}
```

`track` is `bscp`, `security-plus`, or `mixed`. `answer` is the index of the correct choice. `lesson` is optional and must point at a real note. Download from the editor writes this shape. Put the file in `content/quizzes/` and run `bun run build`.

## 🎯 The two tracks

| Track | Route | Families | Start here |
Expand All @@ -244,7 +273,7 @@ Outlines already in the tree (business logic, race conditions, cache poisoning,

1. **Study-level concepts and lab technique.** Describe what to look for and how a tester or defender reasons. Academy labs are where specific syntax belongs, and only against targets you are allowed to test.
2. **No exploit procedures.** No proof-of-concepts, weaponized payloads, malware, copy-paste attack scripts, wordlists, or tool walkthroughs that fire an attack.
3. **No question banks.** No copied CompTIA or PortSwigger exam items, and no original quiz engine or flashcard deck in this app. Drill owns the reps.
3. **No copied exam items.** Quizzes in `content/quizzes`, and quizzes saved in this browser, are original concept checks. Do not paste CompTIA or PortSwigger questions.
4. **Original prose.** Citing a public Academy topic name in `academy:` is fine. Pasting their lab solutions is not.
5. **The build is the editor.** Invalid frontmatter, a family that is not in `src/lib/tracks.ts`, a duplicate `order`, a related slug that does not exist, or a start-here slug that is missing or still an outline fails the build with `Content error:`.
6. **Progress stays on this machine.** `localStorage` only. No accounts in v1.
Expand Down Expand Up @@ -282,9 +311,9 @@ Solid is the theme control. React is search. Vue is the track hub. Svelte is les
</details>

<details>
<summary><b>Can I add a quiz?</b></summary>
<summary><b>How do I add a quiz?</b></summary>

No. Question practice stays in Drill. New files here are reading notes.
Open `/quizzes/new` and save it in this browser, or add `content/quizzes/your-slug.json` and rebuild. The file needs a title, a track (`bscp`, `security-plus`, or `mixed`), a summary, and one to twenty questions. Each question has a prompt, two to five distinct choices, an `answer` index, and an explanation. Prompts stay conceptual. A bad file fails `bun run build` with `Content error`.
</details>

## 📈 Status & roadmap
Expand All @@ -293,6 +322,7 @@ No. Question practice stays in Drill. New files here are reading notes.

- [x] Home, track hubs, lesson pages, search, local progress, dark mode
- [x] Astro islands for React, Vue, Svelte, Solid, Preact, Lit, and Alpine
- [x] Original concept quizzes, plus a browser editor that can download JSON
- [x] BSCP ready notes for the high-yield Academy families and the five Burp tools
- [x] Security+ ready notes across the five SY0-701 style domains
- [ ] Fill the labeled outlines (business logic, race conditions, cache poisoning, host headers, API testing, WebSockets, prototype pollution, and the six Security+ stubs)
Expand Down
18 changes: 17 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,25 @@ import vue from "@astrojs/vue";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "astro/config";

// Astro 7.3 still emits this directive on MDX propagation modules. Head metadata
// is chosen from the ?astroPropagatedAssets id, and Rolldown warns that it may
// drop the directive. Remove it so production builds stay quiet.
function stripUnusedHeadInjectDirective() {
return {
name: "strip-unused-astro-head-inject",
enforce: "post",
transform(code, id) {
if (!id.includes("astroPropagatedAssets") || !code.includes("use astro:head-inject")) {
return;
}
return code.replaceAll(/["']use astro:head-inject["'];?/g, "");
},
};
}

export default defineConfig({
vite: {
plugins: [tailwindcss()],
plugins: [tailwindcss(), stripUnusedHeadInjectDirective()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
Expand Down
41 changes: 41 additions & 0 deletions content/quizzes/boundary-checks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"title": "Boundary checks",
"track": "bscp",
"summary": "Three distinctions from the BSCP notes: who may read a response, what a session proves, and which control stops injected script.",
"lesson": { "track": "bscp", "slug": "access-control" },
"questions": [
{
"prompt": "A script on another site can read an authenticated response from yours. What went wrong?",
"choices": [
"The browser refused to send the request",
"The server told the browser that the other origin may read the response",
"The user followed a link",
"The session cookie was marked Secure"
],
"answer": 1,
"explain": "Cross-origin resource sharing decides whether foreign script may read the response. The request can still be sent. Cross-site request forgery is about an action caused by a request the browser attaches cookies to, not about reading the body."
},
{
"prompt": "Someone is signed in and can open another person's record by changing an identifier. Where is the gap?",
"choices": [
"Authentication, because the session exists",
"Access control on that specific object",
"Transport encryption",
"The absence of a login page"
],
"answer": 1,
"explain": "Authentication already established who they are. The missing check is whether that identity may use that object. That check is access control."
},
{
"prompt": "Untrusted data is written into a page and the browser treats it as script. Which statement matches the notes?",
"choices": [
"A content security policy is the fix, and encoding is optional",
"Encode for the context the data is written into. A tight content security policy is a second layer",
"Hiding the element with CSS contains it",
"Turning off cookies removes the issue"
],
"answer": 1,
"explain": "Output encoding stops the browser from treating data as script. A content security policy can limit what still runs if encoding fails. A policy that allows inline script does not contain the issue."
}
]
}
41 changes: 41 additions & 0 deletions content/quizzes/control-distinctions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"title": "Control distinctions",
"track": "security-plus",
"summary": "Three Security+ style distinctions: disclosure versus change, what a camera is for, and what insurance does to a risk.",
"lesson": { "track": "security-plus", "slug": "security-controls" },
"questions": [
{
"prompt": "A record is shown to someone who should not have it, and the bits themselves are unchanged. Which goal failed?",
"choices": [
"Confidentiality",
"Integrity",
"Availability",
"Authentication"
],
"answer": 0,
"explain": "Confidentiality is about unauthorized disclosure. Integrity is unauthorized or accidental change. Availability is being able to use the system when you should. Confirm the wording against CompTIA's glossary before the exam. This is a study distinction, not an official item."
},
{
"prompt": "A camera records who comes through a door. Which control shape is that?",
"choices": [
"Technical preventive",
"Physical detective",
"Managerial corrective",
"Technical compensating"
],
"answer": 1,
"explain": "A camera records what happened, so it is detective, and it is a physical device. A lock on the same door would be physical preventive. The camera does not stop the entry by itself."
},
{
"prompt": "The team buys insurance for flood damage in the server room. What did they do with the risk?",
"choices": [
"Mitigate it by lowering the chance",
"Transfer the financial impact",
"Avoid it by leaving the room unused",
"Accept it with no further action"
],
"answer": 1,
"explain": "Insurance shifts the financial impact to another party. Mitigation would be raising the equipment or adding a barrier. Avoidance would be not operating in that room. Acceptance is keeping the risk without extra action."
}
]
}
Binary file added docs/readme/quiz.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/components/SiteFooter.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<div class="mx-auto flex w-full max-w-6xl flex-wrap items-center justify-between gap-3 px-4 py-6 text-sm text-muted">
<p>Concepts and lab technique only. Drills stay in Drill with your coach.</p>
<p class="flex gap-4">
<a href="/quizzes" class="underline decoration-line underline-offset-4 hover:text-ink">Quizzes</a>
<a href="/about" class="underline decoration-line underline-offset-4 hover:text-ink">About these notes</a>
<a href="/search" class="underline decoration-line underline-offset-4 hover:text-ink">Search</a>
</p>
Expand Down
1 change: 1 addition & 0 deletions src/components/SiteHeader.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ThemeToggle from "./solid/ThemeToggle";
const links = [
{ href: "/bscp", label: "BSCP" },
{ href: "/security-plus", label: "Security+" },
{ href: "/quizzes", label: "Quizzes" },
{ href: "/about", label: "About" },
];

Expand Down
48 changes: 48 additions & 0 deletions src/components/react/LocalQuizPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useEffect, useState } from "react";

import { readCustomQuiz } from "@/lib/browser-quizzes";
import type { StoredQuiz } from "@/lib/quiz-schema";

import QuizPlayer from "./QuizPlayer";

function quizIdFromHash(hash: string) {
const raw = hash.replace(/^#/, "");
if (!raw) return null;
try {
return decodeURIComponent(raw);
} catch {
return null;
}
}

export default function LocalQuizPlayer() {
const [quiz, setQuiz] = useState<StoredQuiz | null | undefined>(undefined);

useEffect(() => {
function load() {
const id = quizIdFromHash(location.hash);
setQuiz(id ? readCustomQuiz(id) : null);
}
load();
window.addEventListener("hashchange", load);
return () => window.removeEventListener("hashchange", load);
}, []);

if (quiz === undefined) {
return <p className="text-sm text-muted">Loading the quiz saved in this browser.</p>;
}

if (!quiz) {
return (
<p className="text-sm leading-6">
This browser does not have a quiz for that link.{" "}
<a className="underline decoration-line underline-offset-4" href="/quizzes">
Back to quizzes
</a>
.
</p>
);
}

return <QuizPlayer key={quiz.id} quiz={quiz} />;
}
Loading