A security gate for a personal site, built with an AI assistant
I run a personal site, and I built it as practice: a place to try the checks I would want on a real project. This post lists what runs on every change, what that does and does not prove, and what I plan to add next.
How it was built
None of this was written by hand. I built the site and its pipeline by describing what I wanted to an AI assistant, which is often called vibe coding. When tests failed, some of the fixes came the same way.
That changes how I think about the gate. I cannot vouch for code I did not write line by line, but I can vouch for a check that fails when something is wrong. So the pipeline is the evidence, not decoration.
What runs on every change
Everything below except the deployed-site scan runs on each push to main and on each pull request. The security workflow also runs every Monday.
- Type check, unit tests and build. The basics, as one job.
- Build-output tests. These run against the built site and fail if any page has an inline
styleattribute or event handler, an unhashed inline script, a third-party resource or a broken internal link. - The same steps inside the Docker image. A second job repeats them in the container, so the container matches CI.
- Accessibility and quality. The axe scanner audits every built page, end-to-end tests drive a real Chrome, and Lighthouse CI must score 100 for accessibility.
- Dependencies.
npm auditfails on high-severity findings,npm audit signatureschecks the registry signatures, a CycloneDX SBOM is saved as a build artifact, and Dependabot proposes updates each week for both npm packages and GitHub Actions. - Code. CodeQL analyses the JavaScript and TypeScript.
- Secrets. gitleaks scans each change for secrets, with the full git history checked out so it can see every commit.
- The deployed site. A separate workflow runs OWASP ZAP’s baseline scan against
https://sangtran.devevery Wednesday. It is passive: it reads the live responses and checks headers, the content security policy, cookies and mixed content, and sends no attack traffic. It reports only for now, so a finding saves a report but does not fail the run. - Hygiene. Every third-party action is pinned to a full commit SHA, packages install with
npm ci --ignore-scripts, the workflows get read-only permissions, and the development container runs with all Linux capabilities dropped.
A guard for the guard
An assistant that fixes a failing test can also “fix” it by loosening the rule. So some tests guard the configuration itself. They fail if the content security policy gains unsafe-inline or unsafe-eval, if the CodeQL job is given write permission, if the audit step disappears, if any action stops being pinned to a commit, or if the workflows gain a write permission.
That is not airtight. An assistant could edit the guard test as easily as the rule, which is why reviewing changes to tests matters as much as reviewing changes to code.
What it cannot do
- Nothing is enforced. The repository is private on GitHub’s free plan, which does not offer branch protection or rulesets. “Checks must pass before merging” is a habit, not a lock. Whoever has write access could push straight to
mainor merge a red pull request. - CodeQL does not report to GitHub. Uploading code scanning results needs a paid feature on a private repository, so the analysis runs with
upload: neverand findings do not show in the Security tab. I have not confirmed whether a finding alone would fail the job. - Some GitHub features are unavailable for the same reason, such as dependency review and secret-scanning push protection.
- Nothing actively attacks the running site. The ZAP baseline scan only observes responses. No check probes the site for injection or other flaws the way an attacker would, and for a static site with no backend there is little for such a probe to find.
- The SBOM is only an inventory. It is generated and kept as an artifact, but it is not signed or attested, and it does not find vulnerabilities by itself.
What I plan to add
None of these is set up yet. OWASP ZAP is already running as a passive baseline scan; an active scan would only make sense once the site has a server to test.
- SonarCloud for static analysis (SAST), as a second analyser alongside CodeQL.
- Snyk for dependency analysis (SCA), as a second source of vulnerability data next to
npm audit. - Renovate for update pull requests, which should give me more control than Dependabot over how updates are grouped and scheduled.
I still need to find out whether the free tiers of these tools cover a private repository.