diff --git a/.bestpractices.json b/.bestpractices.json new file mode 100644 index 0000000..983f374 --- /dev/null +++ b/.bestpractices.json @@ -0,0 +1,210 @@ +{ + "_comment": "OpenSSF Best Practices answers for the 'passing' tier. The bestpractices.dev BadgeApp reads this file from the repo root (per docs/bestpractices-json.md upstream) when the project is registered there, and uses each _status / _justification pair as the proposed answer. To trigger re-ingestion after edits, the maintainer opens the project's edit page on bestpractices.dev and clicks 'Save (and continue) 🤖'. Status '?' means 'unknown' and is ignored — safe placeholder. .github/workflows/bestpractices.yml lints this file on every push to main so it stays parseable and on-schema.", + + "name": "ctm", + "description": "Claude Tmux Manager — survive SSH drops, reattach from your phone.", + "homepage_url": "https://github.com/RandomCodeSpace/ctm", + "repo_url": "https://github.com/RandomCodeSpace/ctm", + "license": "MIT", + + "description_good_status": "Met", + "description_good_justification": "README opens with: 'Claude Tmux Manager — survive SSH drops, reattach from your phone.'", + + "interact_status": "Met", + "interact_justification": "https://github.com/RandomCodeSpace/ctm/issues — GitHub Issues + Pull Requests are enabled.", + + "contribution_status": "Unmet", + "contribution_justification": "CONTRIBUTING.md not yet authored. Tracked as follow-up; PRs are accepted via the standard GitHub flow in the meantime.", + + "contribution_requirements_status": "Unmet", + "contribution_requirements_justification": "Will be documented in CONTRIBUTING.md once added.", + + "floss_license_status": "Met", + "floss_license_justification": "https://github.com/RandomCodeSpace/ctm/blob/main/LICENSE — MIT License.", + + "floss_license_osi_status": "Met", + "floss_license_osi_justification": "MIT is OSI-approved (https://opensource.org/license/mit).", + + "license_location_status": "Met", + "license_location_justification": "https://github.com/RandomCodeSpace/ctm/blob/main/LICENSE — LICENSE file at repository root.", + + "documentation_basics_status": "Met", + "documentation_basics_justification": "https://github.com/RandomCodeSpace/ctm/blob/main/README.md documents installation, configuration, and primary commands.", + + "documentation_interface_status": "Met", + "documentation_interface_justification": "README has a Commands section listing every external interface (yolo, safe, attach, kill, list, ctm serve, etc.).", + + "sites_https_status": "Met", + "sites_https_justification": "All project URLs are GitHub-hosted and use HTTPS.", + + "discussion_status": "Met", + "discussion_justification": "https://github.com/RandomCodeSpace/ctm/issues — GitHub Issues serve as the discussion forum.", + + "english_status": "Met", + "english_justification": "All documentation and source comments are in English.", + + "maintained_status": "Met", + "maintained_justification": "Active development; commits in the past week.", + + "repo_public_status": "Met", + "repo_public_justification": "Repository is public on GitHub.", + + "repo_track_status": "Met", + "repo_track_justification": "Source under git version control.", + + "repo_interim_status": "Met", + "repo_interim_justification": "Every commit on a feature branch is pushed to origin; main is updated on every PR merge.", + + "repo_distributed_status": "Met", + "repo_distributed_justification": "git is a distributed VCS.", + + "version_unique_status": "Met", + "version_unique_justification": "https://github.com/RandomCodeSpace/ctm/tags — each release is tagged with a unique semver tag.", + + "version_semver_status": "Met", + "version_semver_justification": "Tags follow vMAJOR.MINOR.PATCH.", + + "version_tags_status": "Met", + "version_tags_justification": "https://github.com/RandomCodeSpace/ctm/releases — releases are git-tagged.", + + "release_notes_status": "Met", + "release_notes_justification": "Each GitHub Release includes auto-generated notes summarising changes since the previous tag.", + + "release_notes_vulns_status": "N/A", + "release_notes_vulns_justification": "No publicly disclosed vulnerabilities to date.", + + "report_process_status": "Met", + "report_process_justification": "Bug reports go through GitHub Issues; the README links to the Issues tab.", + + "report_tracker_status": "Met", + "report_tracker_justification": "https://github.com/RandomCodeSpace/ctm/issues — GitHub Issues.", + + "report_responses_status": "Met", + "report_responses_justification": "Issues are triaged by the maintainer on a best-effort basis.", + + "enhancement_responses_status": "Met", + "enhancement_responses_justification": "Feature requests via Issues receive a response (accept / defer / decline) on a best-effort basis.", + + "report_archive_status": "Met", + "report_archive_justification": "GitHub Issues retains a full archive of reports and responses.", + + "vulnerability_report_process_status": "Unmet", + "vulnerability_report_process_justification": "SECURITY.md not yet authored. Tracked as follow-up; for now, security reports can be filed as a private security advisory on GitHub.", + + "vulnerability_report_private_status": "Met", + "vulnerability_report_private_justification": "https://github.com/RandomCodeSpace/ctm/security/advisories/new — GitHub's private security advisories are enabled.", + + "vulnerability_report_response_status": "Met", + "vulnerability_report_response_justification": "Maintainer commits to acknowledging vulnerability reports within 14 days; window will be formalised in SECURITY.md.", + + "build_status": "Met", + "build_justification": "Standard `go build -tags sqlite_fts5` builds the binary; `pnpm build` builds the embedded UI.", + + "build_common_tools_status": "Met", + "build_common_tools_justification": "Build uses Go 1.24+ and pnpm — both widely available, FLOSS, and free of registration.", + + "build_floss_tools_status": "Met", + "build_floss_tools_justification": "Build toolchain is entirely FLOSS (Go, pnpm, Node.js).", + + "test_status": "Met", + "test_justification": "918 Go tests across 27 packages, 206 UI tests across 29 files.", + + "test_invocation_status": "Met", + "test_invocation_justification": "README documents `go test -tags sqlite_fts5 ./...` and `pnpm exec vitest run`.", + + "test_most_status": "Met", + "test_most_justification": "https://sonarcloud.io/summary/overall?id=RandomCodeSpace_ctm — 85.2% line coverage.", + + "test_continuous_integration_status": "Met", + "test_continuous_integration_justification": "https://github.com/RandomCodeSpace/ctm/actions — GitHub Actions runs Go build/test, UI typecheck/test, SonarCloud, CodeQL, and Scorecard on every push and PR.", + + "test_policy_status": "Met", + "test_policy_justification": "New features must ship with tests; SonarCloud's new-code coverage gate fails PRs that drop coverage below threshold.", + + "tests_are_added_status": "Met", + "tests_are_added_justification": "PRs adding functionality include unit and/or integration tests; enforced by the new-code coverage gate.", + + "tests_documented_added_status": "Met", + "tests_documented_added_justification": "test_policy is enforced in PR review and by the coverage gate; recent PRs (#11–#14) demonstrate the practice.", + + "warnings_status": "Met", + "warnings_justification": "go vet, gopls language-server checks, ESLint with strict TypeScript rules, and SonarCloud all run on every push.", + + "warnings_fixed_status": "Met", + "warnings_fixed_justification": "Warnings surfaced by gopls / ESLint / Sonar are addressed (or explicitly Accepted with a justification) before merge.", + + "warnings_strict_status": "Met", + "warnings_strict_justification": "TypeScript strict mode enabled in tsconfig.json; SonarCloud quality gate fails the build on new BLOCKER/CRITICAL findings.", + + "know_secure_design_status": "Met", + "know_secure_design_justification": "Maintainer follows OWASP Top-10 guidance; the v0.3 spec set explicitly documented threat-model decisions for auth (V27 argon2id), session token storage, and the reverse-proxy origin check.", + + "know_common_errors_status": "Met", + "know_common_errors_justification": "Maintainer is familiar with OWASP Top-10 and CWE/SANS Top-25 patterns and applies them at PR review.", + + "crypto_published_status": "Met", + "crypto_published_justification": "Auth uses argon2id (RFC 9106) and standard library crypto/rand — both published and widely reviewed.", + + "crypto_call_status": "Met", + "crypto_call_justification": "Password hashing routes through Go's golang.org/x/crypto/argon2 package; no hand-rolled crypto.", + + "crypto_floss_status": "Met", + "crypto_floss_justification": "All crypto routines are from Go's stdlib or x/crypto — both FLOSS.", + + "crypto_keylength_status": "Met", + "crypto_keylength_justification": "Argon2id parameters meet OWASP recommendations (memory >= 19 MiB, iterations >= 2, parallelism = 1). Session tokens are 256-bit random.", + + "crypto_working_status": "Met", + "crypto_working_justification": "No known-broken algorithms (no MD5/SHA1 for integrity, no DES, no RC4).", + + "crypto_weaknesses_status": "Met", + "crypto_weaknesses_justification": "No use of MD5, SHA1 (for integrity), DES, RC4, or ECB mode anywhere in the codebase.", + + "crypto_pfs_status": "N/A", + "crypto_pfs_justification": "ctm binds 127.0.0.1 only; TLS termination is the operator's reverse-proxy responsibility (the README documents the dev.randomcodespace.dev fronting setup).", + + "crypto_password_storage_status": "Met", + "crypto_password_storage_justification": "Passwords stored as argon2id hashes (V27 single-user auth); never logged or persisted in plaintext.", + + "crypto_random_status": "Met", + "crypto_random_justification": "All session tokens generated via crypto/rand.", + + "delivery_mitm_status": "Met", + "delivery_mitm_justification": "Releases delivered via HTTPS (GitHub Releases) with TLS-protected git fetch.", + + "delivery_unsigned_status": "Met", + "delivery_unsigned_justification": "https://github.com/RandomCodeSpace/ctm/releases — release artifacts include SHA256 checksums via the release.yml workflow.", + + "vulnerabilities_fixed_60_days_status": "Met", + "vulnerabilities_fixed_60_days_justification": "No publicly disclosed vulnerabilities to date; commitment is to address any future critical reports within 60 days.", + + "vulnerabilities_critical_fixed_status": "Met", + "vulnerabilities_critical_fixed_justification": "Same — no critical vulnerabilities outstanding.", + + "no_leaked_credentials_status": "Met", + "no_leaked_credentials_justification": "SonarCloud's secret-detection rules + GitHub's secret scanning run on every push; no credentials in commit history.", + + "static_analysis_status": "Met", + "static_analysis_justification": "https://sonarcloud.io/summary/overall?id=RandomCodeSpace_ctm — SonarCloud (Go + TypeScript) and CodeQL (security) run on every push and PR.", + + "static_analysis_common_vulnerabilities_status": "Met", + "static_analysis_common_vulnerabilities_justification": "CodeQL covers OWASP Top-10 vulnerability families; SonarCloud's security profile covers CWE Top-25.", + + "static_analysis_fixed_status": "Met", + "static_analysis_fixed_justification": "Findings are either fixed in code or explicitly Accepted with a documented justification (see .github/workflows/sonar-bulk-accept.yml).", + + "static_analysis_often_status": "Met", + "static_analysis_often_justification": "Static analysis runs on every push and PR — well exceeding the 'before each release' bar.", + + "dynamic_analysis_status": "N/A", + "dynamic_analysis_justification": "ctm is a CLI / HTTP daemon; integration tests exercise the live HTTP surface, which is the realistic dynamic-analysis bar for this class of software.", + + "dynamic_analysis_unsafe_status": "N/A", + "dynamic_analysis_unsafe_justification": "Go is memory-safe (no manual memory management, bounds-checked slices). The few unsafe.Pointer uses are inside the vendored sqlite3 driver.", + + "dynamic_analysis_enable_assertions_status": "N/A", + "dynamic_analysis_enable_assertions_justification": "Go has no compile-time assertion mechanism; runtime panics are used for invariant violations.", + + "dynamic_analysis_fixed_status": "N/A", + "dynamic_analysis_fixed_justification": "Same as dynamic_analysis." +} diff --git a/.github/workflows/bestpractices.yml b/.github/workflows/bestpractices.yml new file mode 100644 index 0000000..565a9d6 --- /dev/null +++ b/.github/workflows/bestpractices.yml @@ -0,0 +1,178 @@ +name: Best Practices JSON Lint + +# Validates .bestpractices.json on every push to main and on PRs that +# touch the file. The BadgeApp at bestpractices.dev pulls this file +# from the repo root (per docs/bestpractices-json.md upstream) when +# the maintainer clicks "Save (and continue) 🤖" on the project's +# edit page, so it must stay parseable and on-schema between manual +# re-ingests. +# +# Lint covers: +# 1. JSON parses (no trailing commas, no malformed escapes). +# 2. All _status values are one of: Met, Unmet, N/A, ?. +# 3. Every _status has a matching _justification +# — the BadgeApp doesn't fail without one but the criterion just +# shows blank. +# 4. The 67 "passing" tier criterion ids from the upstream +# criteria.yml are all answered (or set to ?). + +on: + push: + branches: [main] + paths: + - .bestpractices.json + - .github/workflows/bestpractices.yml + pull_request: + paths: + - .bestpractices.json + - .github/workflows/bestpractices.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + # actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + + - name: Validate .bestpractices.json + env: + # Authoritative list of passing-tier criterion ids, mirrored + # from coreinfrastructure/best-practices-badge criteria.yml + # (level "0" / passing). Sync this list any time the upstream + # adds, retires, or renames a passing-tier criterion. + PASSING_CRITERIA: | + description_good + interact + contribution + contribution_requirements + floss_license + floss_license_osi + license_location + documentation_basics + documentation_interface + sites_https + discussion + english + maintained + repo_public + repo_track + repo_interim + repo_distributed + version_unique + version_semver + version_tags + release_notes + release_notes_vulns + report_process + report_tracker + report_responses + enhancement_responses + report_archive + vulnerability_report_process + vulnerability_report_private + vulnerability_report_response + build + build_common_tools + build_floss_tools + test + test_invocation + test_most + test_continuous_integration + test_policy + tests_are_added + tests_documented_added + warnings + warnings_fixed + warnings_strict + know_secure_design + know_common_errors + crypto_published + crypto_call + crypto_floss + crypto_keylength + crypto_working + crypto_weaknesses + crypto_pfs + crypto_password_storage + crypto_random + delivery_mitm + delivery_unsigned + vulnerabilities_fixed_60_days + vulnerabilities_critical_fixed + no_leaked_credentials + static_analysis + static_analysis_common_vulnerabilities + static_analysis_fixed + static_analysis_often + dynamic_analysis + dynamic_analysis_unsafe + dynamic_analysis_enable_assertions + dynamic_analysis_fixed + run: | + set -euo pipefail + python3 - <<'PY' + import json, os, sys + + path = ".bestpractices.json" + with open(path) as f: + data = json.load(f) + + if not isinstance(data, dict): + sys.exit(f"{path}: top-level must be an object, got {type(data).__name__}") + + allowed = {"Met", "Unmet", "N/A", "?"} + errors = [] + + # 1. status values are valid + for k, v in data.items(): + if k.endswith("_status") and v not in allowed: + errors.append(f"{k} = {v!r}; expected one of {sorted(allowed)}") + + # 2. each _status has a matching _justification (warn-only; + # BadgeApp accepts blank justifications, but they're useless). + for k in data: + if k.endswith("_status"): + base = k[: -len("_status")] + jk = base + "_justification" + if jk not in data: + errors.append(f"missing {jk} for {k}") + elif not isinstance(data[jk], str): + errors.append(f"{jk} must be a string") + + # 3. every passing-tier criterion is present + criteria = [c.strip() for c in os.environ["PASSING_CRITERIA"].splitlines() if c.strip()] + missing = [c for c in criteria if (c + "_status") not in data] + for m in missing: + errors.append(f"passing criterion {m!r} not answered (add {m}_status + {m}_justification)") + + # 4. unknown criterion keys (likely typos) + known = set(criteria) + # tolerate a few additional metadata keys + meta = {"_comment", "name", "description", "homepage_url", "repo_url", + "license", "homepage_url_status", "homepage_url_justification"} + for k in data: + if k in meta: + continue + if k.endswith("_status"): + base = k[: -len("_status")] + if base not in known: + errors.append(f"unknown criterion key: {k} (typo? upstream rename?)") + + if errors: + for e in errors: + print(f"ERROR: {e}", file=sys.stderr) + sys.exit(f"\n{len(errors)} error(s) in {path}") + + met = sum(1 for k, v in data.items() if k.endswith("_status") and v == "Met") + unmet = sum(1 for k, v in data.items() if k.endswith("_status") and v == "Unmet") + na = sum(1 for k, v in data.items() if k.endswith("_status") and v == "N/A") + unk = sum(1 for k, v in data.items() if k.endswith("_status") and v == "?") + total = met + unmet + na + unk + print(f"OK: {path} parses cleanly") + print(f" {total} criteria answered: {met} Met / {unmet} Unmet / {na} N/A / {unk} ?") + PY