cmd/fort/ main binary (cobra CLI)
main.go flags, run() entrypoint, --only filtering
output.go terminal output + JSON serialisation
report.go HTML report template + writeReport()
*_test.go unit tests — no real OS checks in tests
cmd/sample-report/ demo report generator (landing page)
main.go randomised fake results + same HTML template
cmd/landing/ landing page server (local dev only)
internal/checks/ all security checks
check.go Result struct, Check interface, Status consts
evidence.go evidenceCmd() helper — runs commands, captures transcripts
registry_darwin.go ordered list of all checks (the canonical run order)
frameworks.go SOC 2 / ISO 27001 / NIST / CIS control mappings
*_darwin.go one file per check
landing/ GitHub Pages site (deployed on push to main)
index.html marketing page
sample-report.html generated by cmd/sample-report, committed here
- No hidden state. Every check is a pure
Run() Result— reads system state, returns a struct. No caching, no side effects outsideFix(). - Evidence is always captured. Every
Run()must setResult.EvidenceusingevidenceCmd(). This is the raw terminal transcript auditors see. Never leave it empty. - Template duplication is intentional.
cmd/fort/report.goandcmd/sample-report/main.goboth containreportTmpl. Keep them in sync manually — the duplication avoids a shared internal package for what is essentially a one-file template.
The check count appears in several places. This checklist keeps them in sync, and two automated guards (below) fail CI if you miss a step.
-
Create
internal/checks/yourcheck_darwin.goand implement theCheckinterface:ID(): short stable slug (e.g."screensaver")Name(): human display name. Match official product spelling (e.g. Apple's "Touch ID", not "TouchID").Run() Result: useevidenceCmd()to run the system command and setEvidenceFixable() bool,Fix() error,FixDescription() string
-
Register in
internal/checks/registry_darwin.go(order matters; it's the report order). -
Add framework mappings in
internal/checks/frameworks.go. -
Add a fake result + evidence string in
cmd/sample-report/main.gorandomResults(), and add it to the returned slice. Use the sameName()as the real check. -
Bump the count in
internal/checks/checks_test.go(const want = N). This is the single guard for the registry size;go testfails until you update it. -
Add a
<div class="check-item">to the "What it checks" grid inlanding/index.html. (The headline counts say "15+", so no number to change there, just list the check.) -
Run
make gento regeneratelanding/sample-report.html, then commit it. -
Run
go test ./.... The interface contract, report rendering, and count guard are covered.
TestRegistryCount(internal/checks/checks_test.go): fails if the number of registered checks doesn't matchconst want. Forces step 5.make check-gen(also run in CI): regenerates the sample report and fails if the committedlanding/sample-report.htmldiffers. Forces step 7. The generator is deterministic (fixed seed + timestamp), so a clean regen is always byte-identical.
landing/index.htmlcheck grid and terminal demo (step 6): hand-maintained HTML.- Marketing assets (hero gif, OG card, screenshots): regenerated from the landing page
with the capture tooling, then dropped into
docs/.
checks_test.go — interface contract tests. Every check in All() must satisfy:
- Returns a non-empty
ID()andName() Run()returns a validStatus(pass/fail/warn)Run()setsEvidence(non-empty string)Fixable()andFix()are consistent
report_test.go covers writeReport():
TestWriteReport— HTML structure, machine metadata, check names, statuses, frameworks, scoreTestWriteReportAllPass— 100% score class (s-pass)TestWriteReportFixed—fixedbadge rendersTestWriteReportEvidence—Evidencefield renders as<pre class="ev-pre">, exact count matches number of checks with evidence, checks without evidence don't get the toggleTestWriteReportInvalidPath— error on bad path
output_test.go covers CLI output and run():
TestTally— pass/fail/warn countingTestAnyFixable— fixable detectionTestStatusIcon— terminal icon mappingTestToJSONPolicies— framework enrichment in JSONTestPrintJSON— valid JSON output, required fieldsTestPrintHuman— human output contains check names, score, hostTestVersion— semver formatTestRunDryRun— dry-run path is read-only, exits 0TestRunJSONOutput—--jsonproduces parseable outputTestRunOnly—--only filevaultproduces exactly 1 policy with correct IDTestRunOnlyUnknownID— unknown ID warns to stderr
--fixinteractive prompt — requires a TTY; test manually withfort --fix- Real OS check values — these are integration tests; run
fortitself
When changing the HTML/CSS in cmd/fort/report.go, apply the same change to cmd/sample-report/main.go. Both contain an identical reportTmpl const. The key sections to keep in sync:
- CSS variables and all style rules
- Evidence
<details class="ev">block in the check results table HasEvidencefield onpolicyRowand population inwriteReport()
Run make gen after changes to regenerate landing/sample-report.html.
make gen # regenerates landing/sample-report.html (deterministic)
git add landing/sample-report.html
git commit -m "Refresh sample report"
git push
# GitHub Pages deploys automatically (pages.yml workflow, ~30s)The generator uses a fixed seed and timestamp, so the output is reproducible. If
make gen produces a diff you didn't expect, the template or a check changed.
Released binaries get their version from the git tag, injected at build time by
GoReleaser (release) and make (local, via git describe). So the tag is the source
of truth. A few spots still hardcode the version and should track the latest release:
cmd/fort/main.go:var version = "x.y.z"(default when not built via make/release)cmd/sample-report/main.go:Version: "x.y.z"in the sample report dataREADME.md: the static release badgerelease-vX.Y.Z(static so it never hits the shields.io GitHub token pool, but that means it is manual)
The landing page demo (landing/index.html) shows the version too; keep it current.
To cut a release, tag and push, and GoReleaser does the rest:
git tag v0.x.0 && git push origin v0.x.0