Skip to content

Latest commit

 

History

History
151 lines (111 loc) · 6.94 KB

File metadata and controls

151 lines (111 loc) · 6.94 KB

Contributing to fort

Project layout

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

Core design rules

  • No hidden state. Every check is a pure Run() Result — reads system state, returns a struct. No caching, no side effects outside Fix().
  • Evidence is always captured. Every Run() must set Result.Evidence using evidenceCmd(). This is the raw terminal transcript auditors see. Never leave it empty.
  • Template duplication is intentional. cmd/fort/report.go and cmd/sample-report/main.go both contain reportTmpl. Keep them in sync manually — the duplication avoids a shared internal package for what is essentially a one-file template.

Adding or changing a check

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.

  1. Create internal/checks/yourcheck_darwin.go and implement the Check interface:

    • 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: use evidenceCmd() to run the system command and set Evidence
    • Fixable() bool, Fix() error, FixDescription() string
  2. Register in internal/checks/registry_darwin.go (order matters; it's the report order).

  3. Add framework mappings in internal/checks/frameworks.go.

  4. Add a fake result + evidence string in cmd/sample-report/main.go randomResults(), and add it to the returned slice. Use the same Name() as the real check.

  5. Bump the count in internal/checks/checks_test.go (const want = N). This is the single guard for the registry size; go test fails until you update it.

  6. Add a <div class="check-item"> to the "What it checks" grid in landing/index.html. (The headline counts say "15+", so no number to change there, just list the check.)

  7. Run make gen to regenerate landing/sample-report.html, then commit it.

  8. Run go test ./.... The interface contract, report rendering, and count guard are covered.

Automated guards

  • TestRegistryCount (internal/checks/checks_test.go): fails if the number of registered checks doesn't match const want. Forces step 5.
  • make check-gen (also run in CI): regenerates the sample report and fails if the committed landing/sample-report.html differs. Forces step 7. The generator is deterministic (fixed seed + timestamp), so a clean regen is always byte-identical.

Not auto-checked (do by hand)

  • landing/index.html check 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/.

Test harness

internal/checks/

checks_test.go — interface contract tests. Every check in All() must satisfy:

  • Returns a non-empty ID() and Name()
  • Run() returns a valid Status (pass/fail/warn)
  • Run() sets Evidence (non-empty string)
  • Fixable() and Fix() are consistent

cmd/fort/

report_test.go covers writeReport():

  • TestWriteReport — HTML structure, machine metadata, check names, statuses, frameworks, score
  • TestWriteReportAllPass — 100% score class (s-pass)
  • TestWriteReportFixedfixed badge renders
  • TestWriteReportEvidenceEvidence field renders as <pre class="ev-pre">, exact count matches number of checks with evidence, checks without evidence don't get the toggle
  • TestWriteReportInvalidPath — error on bad path

output_test.go covers CLI output and run():

  • TestTally — pass/fail/warn counting
  • TestAnyFixable — fixable detection
  • TestStatusIcon — terminal icon mapping
  • TestToJSONPolicies — framework enrichment in JSON
  • TestPrintJSON — valid JSON output, required fields
  • TestPrintHuman — human output contains check names, score, host
  • TestVersion — semver format
  • TestRunDryRun — dry-run path is read-only, exits 0
  • TestRunJSONOutput--json produces parseable output
  • TestRunOnly--only filevault produces exactly 1 policy with correct ID
  • TestRunOnlyUnknownID — unknown ID warns to stderr

What is NOT unit-tested (by design)

  • --fix interactive prompt — requires a TTY; test manually with fort --fix
  • Real OS check values — these are integration tests; run fort itself

Report template sync checklist

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
  • HasEvidence field on policyRow and population in writeReport()

Run make gen after changes to regenerate landing/sample-report.html.

Deploying the sample report

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.

Version bumping

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:

  1. cmd/fort/main.go: var version = "x.y.z" (default when not built via make/release)
  2. cmd/sample-report/main.go: Version: "x.y.z" in the sample report data
  3. README.md: the static release badge release-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