Many organisations have a need for initial security training, and an LMS that accepts SCORM. This means that they can show their auditor(s) for SOC2 and ISO27001 that they're providing relevant training for secure coding practices, and now AI agents. This course is an attempt to do this publicly so multiple organisations can collaborate at making this training useful, rather than just toil for compliance.
A SCORM 1.2 e-learning course on secure code development, interleaving the OWASP Top 10:2025 (classic application security) with the OWASP Top 10 for Agentic Applications 2026 (AI-agent security). It is organised into seven themes, each with a quiz, plus a final comprehensive quiz. The audience is engineers building software with agents and engineers building agents.
This repository holds the course content, spec, and deliverable — not the Xerte Online Toolkits (XOT) software itself. The XOT CMS runs from a separate repo (see Run the CMS).
This course content is licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) — see LICENSE. The ShareAlike clause is required by the source OWASP Top 10 for Agentic Applications 2026 (CC BY-SA 4.0). It is a derivative work: changes were made, and it is not endorsed by OWASP. Full attribution is in Sources and license and on the course's About/References pages.
secure-code-training/
├── README.md # this file
├── COMMIT_MESSAGE.md # suggested initial commit message (credits model + harness)
├── .github/workflows/
│ ├── preview.yml # on PR: renders a readable HTML preview for reviewers
│ └── release.yml # on release: builds the SCORM zip and attaches it
├── tools/
│ └── render_preview.py # renders source/data.xml → preview/index.html
├── docs/ # all documentation
│ ├── COURSE_SPEC.md # the full page-by-page spec (source of truth for content)
│ ├── COURSE_VERIFICATION.md# verification of the export against the spec
│ ├── PROJECT_CONTEXT.md # project-specific context for contributors/bots
│ ├── AGENT_COURSE_GUIDE.md # general guidance for authoring in the Xerte editor
│ └── RESTORE_GUIDE.md # how to restore/import the course into a fresh XOT
└── source/ # Xerte project source (to edit inside the CMS)
├── data.xml # published content (what plays / exports)
└── preview.xml # editor working copy
- The SCORM zip is NOT committed. It is built by the
release workflow from
source/data.xmlusing a real XOT instance, and attached to the GitHub Release. To get the deliverable, publish a release (or run the workflow manually) and download theSecure_code_development_scorm.zipasset. If you only want to deliver the course, upload that asset to your LMS. source/data.xml+source/preview.xmlare the Xerte project content, for editing the course inside a running XOT instance and re-exporting (see Edit & re-export).
Every PR runs the preview workflow, which
renders source/data.xml into a single readable HTML page (all pages + quizzes,
correct answers marked ✓) using tools/render_preview.py
and uploads it as the course-preview artifact, then comments on the PR
with a summary (page/question counts, pass mark, any empty options) and a link
to the artifact. Reviewers download the artifact and open index.html to read
the new content version without running Xerte.
The XOT CMS is not vendored here. It lives in the docker-container branch of
the XOT repo:
- Repo: https://github.com/RichardoC/xerteonlinetoolkits
- Branch:
docker-container - Image tag:
xerte(built from that branch'sDockerfile)
Build the image once, then run it with persistent named volumes:
git clone -b docker-container https://github.com/RichardoC/xerteonlinetoolkits.git
cd xerteonlinetoolkits
docker build -t xerte .
docker run -d --name xerte -p 8081:80 \
-v xerte-data:/var/lib/mysql \
-v xerte-files:/var/www/xerte/USER-FILES \
-e XERTE_FRESHCLAM_ON_START=false \
xerte
# wait ~25s for first-run provisioning, then:
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8081/ # expect 200Guest auth is on by default — visit http://localhost:8081/ and you are
auto-logged-in as guest2 (no login form). The course is owned by guest2.
The host port (8081 above) can be any free port; use it consistently.
docker stop xerte/docker start xertepreserves everything (named volumes).docker rm -f xerte+docker volume rm xerte-data xerte-fileswipes it.
Full instructions are in docs/RESTORE_GUIDE.md. The
short version for a fresh instance:
# 1. Create the (empty) project and note the returned template_id
COOKIE=/tmp/x.cookies
curl -s -c "$COOKIE" http://localhost:8081/index.php -o /dev/null
curl -s -b "$COOKIE" -c "$COOKIE" -X POST \
-d "templatename=Nottingham&tutorialname=Secure code development" \
http://localhost:8081/website_code/php/templates/new_template.php
# -> response like "1,1280, 768"; the first number is the template_id
# 2. Copy the source content into the new project folder and fix perms
ID=1 # replace with the template_id from step 1
FOLDER="/var/www/xerte/USER-FILES/${ID}-guest2-Nottingham"
docker cp source/data.xml "xerte:${FOLDER}/data.xml"
docker cp source/preview.xml "xerte:${FOLDER}/preview.xml"
docker exec xerte sh -c "chown www-data:www-data '${FOLDER}/data.xml' '${FOLDER}/preview.xml'; chmod 664 '${FOLDER}/data.xml' '${FOLDER}/preview.xml'; rm -f '${FOLDER}/lockfile.txt'"
# 3. Make it viewable and verify
docker exec xerte sh -c "mariadb --socket=/run/mysqld/mysqld.sock -uroot -proot xerte -e \"UPDATE templatedetails SET access_to_whom='Public' WHERE template_id=${ID};\""
curl -s -o /dev/null -w "play=%{http_code}\n" "http://localhost:8081/play.php?template_id=${ID}" # expect 200Then open http://localhost:8081/edithtml.php?template_id=${ID} (the HTML5
editor — not /edit.php, which is the legacy Flash editor), confirm the
page tree shows all 48 pages, and click Publish once so XOT stamps its
internal state.
- Start XOT and import the course as above (or
docker start xerteif the project already exists in the volumes). - Sync the editor's working copy to the latest published content:
docker exec xerte sh -c 'cp /var/www/xerte/USER-FILES/<ID>-guest2-Nottingham/data.xml /var/www/xerte/USER-FILES/<ID>-guest2-Nottingham/preview.xml; rm -f /var/www/xerte/USER-FILES/<ID>-guest2-Nottingham/lockfile.txt' - Open the editor at
/edithtml.php?template_id=<ID>and make changes. Seedocs/AGENT_COURSE_GUIDE.mdfor the authoring workflow (page types, quizzes, CKEditor, gotchas) anddocs/PROJECT_CONTEXT.mdfor the conventions specific to this course. - Publish (only Publish writes
data.xml, which is what plays/exports). - Update
source/data.xmlandsource/preview.xmlfrom the running instance (this is what gets committed and what the release workflow builds the SCORM package from):docker exec xerte cat /var/www/xerte/USER-FILES/<ID>-guest2-Nottingham/data.xml > source/data.xml docker exec xerte cat /var/www/xerte/USER-FILES/<ID>-guest2-Nottingham/preview.xml > source/preview.xml
- (Optional, local sanity) Re-export SCORM to verify the build:
COOKIE=/tmp/x.cookies curl -s -c "$COOKIE" http://localhost:8081/index.php -o /dev/null curl -sL -b "$COOKIE" -o /tmp/Secure_code_development_scorm.zip \ "http://localhost:8081/website_code/php/scorm/export.php?scorm=true&template_id=<ID>" unzip -l /tmp/Secure_code_development_scorm.zip | head
- Commit the updated
source/,tools/, and any doc changes. Do not commit the SCORM zip — it is built by the release workflow and is in.gitignore. Open a PR; the preview workflow will render the new content for reviewers. - When the PR is merged and you want to publish, create a GitHub Release; the release workflow builds the SCORM zip and attaches it to the release.
These are binding on this derivative work — see
docs/PROJECT_CONTEXT.md for the full list:
- License: the course is CC BY-SA 4.0 (required by the Agentic PDF's ShareAlike clause). Keep the attribution, "changes were made", "not endorsed by OWASP", and provenance notices on the About and References pages.
- Acronyms: expand on first use per page, e.g.
TOCTOU (Time-of-check to time-of-use). - Quizzes: full descriptive names in question stems (never "What causes
A01" — OWASP renumbers between releases); no free-text questions; no empty
answer options; questions nested under their
<quiz>node. - No auto-playing text: content pages show all content at once (Plain Text,
or Bullets with
delaySecs="0"). - Incident callouts must be self-contained for learners who haven't read the OWASP docs; incident references include hyperlinked sources.
After any change, re-export and run the checks in
docs/PROJECT_CONTEXT.md
(question count, pass mark, no empty options, play.php returns 200) and walk a
quiz end-to-end in play.php (answer → Check → Next → complete → Restart).
This course is a derivative work based on:
- OWASP Top 10 for Agentic Applications 2026 — OWASP Gen AI Security Project, Agentic Security Initiative, https://genai.owasp.org — CC BY-SA 4.0
- OWASP Top 10:2025 — OWASP Top 10 Team, 2021–2025, https://owasp.org/Top10/2025/ — CC BY 3.0
The course is licensed CC BY-SA 4.0. Changes were made; it is not endorsed by OWASP.