[BR|EN]~/samuel-felix

cat cases/guardiao-impacta.md

Guardião Impacta

Security awareness app built as my capstone project and used by real participants in a field study.

Summary

Guardião Impacta (“Impacta Guardian”) was a mobile web app (PWA) that taught information security to people who don’t work in IT, through short lessons, trivia, points, achievements and a leaderboard. It was my group’s capstone project for a Computer Science degree, and it ran in production while the study collected data, until November 2, 2026. The app was then shut down.

Automated tests 475 on the API (100% line coverage) and 385 on the UI (99.8%)
Load test 100 concurrent users for 3 minutes: zero failures, 95% of responses within 20 ms; up to 41 ms in a 1,000-user stress test
Security 6 issues found in code review and fixed; OWASP ZAP with zero failures afterwards
Delivery every push to main with a green CI went to production on its own, with no SSH port open

Context

Security awareness programs tend to have low engagement, especially among non-technical staff. The study asked whether a gamified app is more engaging than the traditional trainings the participants had already taken.

The capstone was a five-person group project. My part was building the whole app (UI, API and database), the infrastructure, the CI/CD, and writing the thesis. I authored 156 of the repository’s 159 commits.

Architecture

Diagram, labeled in Portuguese: participants and admins reach Caddy over HTTPS, on an Azure VM running Docker Compose; Caddy forwards /api to the Node.js API, which uses PostgreSQL and sends emails over SMTP. Developers push to GitHub, and GitHub Actions tests and deploys to the VM.
Production architecture (figure from the thesis, in Portuguese).

Engineering decisions

Scoring enforced by the database, not the phone. Points drove the leaderboard and fed the study data. Computed on the device, they could be tampered with. So the rule lived on the server: FOR UPDATE transactions kept two requests for the same round from running in parallel, and partial unique indexes let each question score only once per person, even with repeated taps or concurrent requests. Integration tests covered these races.

Session checked against the database on every request. The JWT only identified the user; the middleware loaded the user from the database each time. Deactivating an account or changing its role took effect immediately, and changing the password invalidated older tokens.

Versioned schema. The database was defined by numbered SQL migrations, applied in order when the API started. An applied migration was never edited; every change became a new file.

Details in the project README (in Portuguese).

How content was added

Writing a lesson required no database or programming: each lesson was a folder with one HTML file.

conteudo/aulas/          (lessons)
  16-new-topic/
    aula.html            title, text and questions
    imagens/             images used in the lesson
  1. Copy conteudo/modelo/ (the template) to conteudo/aulas/16-new-topic/. The number sets the lesson’s place in the track.
  2. Edit aula.html. Each question has 4 options, one marked as correct, and an explanation shown after the answer (attributes are in Portuguese):
<div data-pergunta="password-request" data-pontos="10">
  <p data-enunciado>Someone calls claiming to be IT support and asks for your password. What do you do?</p>
  <ol>
    <li>Give the password, since it's support</li>
    <li>Give only half of it</li>
    <li data-correta>Refuse, hang up and report it to security</li>
    <li>Ask them to send an email first</li>
  </ol>
  <p data-explicacao>No legitimate person asks for your password. Refuse and report.</p>
</div>
  1. Open the file in a browser to preview it with the app’s look.
  2. Run the validator. If something is wrong, it names the file, the question and the problem:
$ docker compose exec api npm run importar-aulas -- --validar
04-engenharia-social/aula.html: pergunta "pedido-de-senha": precisa de exatamente 1 alternativa com data-correta, tem 2
(needs exactly 1 option with data-correta, has 2)
  1. Push. CI ran the same validator and, if everything was green, the deploy shipped it. The API imported the lesson on startup without losing answers or points already earned.

Trivia questions were added through the app’s admin area, with no code: prompt, options, explanation, level (easy, medium or hard) and a reference lesson, which became a “Review lesson” link when someone answered wrong.

That made content versioned and tested just like code, and lesson authors never had to touch the database. The full guide is in conteudo/README.md (in Portuguese).

Continuous delivery

Every push ran this sequence on GitHub Actions:

lint and format           eslint + prettier
caddy validate            a broken Caddyfile would take the site down
lessons                   the validator from the previous section
API tests                 against a real Postgres, 80% minimum coverage
UI tests and build        80% minimum coverage
deploy (main only)        Azure login via OIDC → Run Command on the VM → deploy.sh

AI agents under control

I used Claude Code as a development agent. An agent with terminal and cloud access needs clear limits, and those limits were versioned in the repository:

The thesis discloses the use of generative AI in development and in proofreading (section 1.6).

Quality and performance

Security

On October 1, 2026, midway through development, I ran a security pass with Claude Code: a review of the API and UI code, followed by four scanners against the production stack running locally.

The code review found 6 issues, all fixed with tests in commit 1b8948c:

Severity Issue Fix
Medium Each lesson’s HTML, answer key included, was public with no login The route now serves images only
Low-medium A telemetry event starting with - became a formula in the CSV exported to Excel Anything starting with - that isn’t a number gets escaped
Low With SMTP off in production, the password reset link went to the log In production the email is dropped without logging the link
Low Telemetry and trivia accepted 300 requests per minute A 60-per-minute limit per user
Low The browser cached API responses on disk Cache-Control: no-store on /api
Low The password reset token stayed in browser history The token is removed from the URL as soon as the page opens

Then the scanners:

Tool Type Result
OWASP ZAP, passive scan DAST 0 failures, 5 warnings, 62 rules with no alert
OWASP ZAP, active scan DAST 0 failures, 2 warnings (headers), 139 rules with no alert
Semgrep (601 rules) SAST 0 issues in the code; 4 configuration alerts (development Dockerfiles and actions not pinned by SHA)
Trivy dependencies and images 0 vulnerabilities in the app’s dependencies; 19 CVEs in the API image, all in the npm bundled with the Node image
Gitleaks secrets 1 alert, a false positive (empty JWT_SECRET= in .env.example)

npm was removed from the API’s production image (0dcca2d), and Trivy then found 0 CVEs in it. The Caddy image’s CVEs were in its binary; the deploy now rebuilds images with --pull on every release. The pass also caught an availability bug: with the database down, the API crashed instead of returning 503. That was fixed in cb715eb.

Limits: ZAP only tested public pages, since it can’t log in on its own. The HTML and JSON reports were lost with the temporary folder, and the numbers above come from the session log. The reports are not published; the fixes are in the linked commits.

Study and privacy

Limits and what I would do differently

Screens

The app’s interface is in Portuguese.