mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 20:47:17 +01:00
The leak this repository had was not carelessness with a secret. It was somebody reaching for a real name because it was the example in front of them -- a fixture built from a live database, an envelope pasted out of a running container, a help text written while looking at the actual group. Three different people-shaped mistakes, none of which a rule about "be careful" would have stopped. So it is a gate. The hard part is that the thing being checked must not contain the list of what to refuse, or the check *is* the leak. It reads the names at run time from outside: the photo pack's manifest, the message database, the local .env. It never prints the word it matched, because this output reaches CI logs. On a machine with none of that private data it says "shapes only" out loud rather than passing quietly, and CI enforces the half that needs nothing -- phone numbers, uuids and Signal group ids, compared against an allowlist of values invented for the fixtures. It runs on staged files, on the commit message -- where two of the leaks actually travelled -- and over the whole tree in CI. It paid for itself twice before it was wired up. The live test group's id was in tests/test_config.py in two spellings, and truncated in three more files, after four rounds of grepping by hand had pronounced the tree clean -- nobody thinks to grep for base64. Then it refused the commit that introduced it, because DOCS-privacy.md had used a real name as the example of a name. An allowlist line may be scoped to one file, which is how the given names that are also popes stay legal in popes.json and refused in every fixture that has no business carrying them. The threshold is three characters, because one of this group's given names is three characters long. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017w1HZ3cZ7neL3rFdQ6VMBA
149 lines
5.3 KiB
YAML
149 lines
5.3 KiB
YAML
# Pre-commit hooks for signal-bot.
|
|
#
|
|
# Install with: pre-commit install
|
|
# Run manually: pre-commit run --all-files
|
|
#
|
|
# Every hook is `language: system` because the tools already live in .venv;
|
|
# pre-commit must not build isolated environments for them. fail_fast so a
|
|
# later check cannot run after an earlier one has already rejected the commit.
|
|
fail_fast: true
|
|
|
|
repos:
|
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
rev: v4.6.0
|
|
hooks:
|
|
- id: trailing-whitespace
|
|
args: [--markdown-linebreak-ext=md]
|
|
- id: end-of-file-fixer
|
|
- id: check-yaml
|
|
- id: check-json
|
|
- id: check-toml
|
|
- id: check-merge-conflict
|
|
- id: check-case-conflict
|
|
- id: check-executables-have-shebangs
|
|
- id: check-shebang-scripts-are-executable
|
|
- id: detect-private-key
|
|
- id: debug-statements
|
|
- id: check-ast
|
|
- id: mixed-line-ending
|
|
args: [--fix=lf]
|
|
|
|
- repo: local
|
|
hooks:
|
|
# Suppressions are blocked mechanically, not by discipline. This repo
|
|
# has zero of them and the only way it stays that way is a gate: the
|
|
# rule is "fix the finding, or ask first", and a habit is not a gate.
|
|
- id: no-noqa
|
|
name: block noqa / type:ignore comments
|
|
entry: '(?i)#\s*(noqa|type:\s*ignore)'
|
|
language: pygrep
|
|
types: [python]
|
|
|
|
- id: no-ruff-noqa
|
|
name: block file-level ruff noqa
|
|
entry: '(?i)#\s*ruff:\s*noqa'
|
|
language: pygrep
|
|
types: [python]
|
|
|
|
# This repository is public and holds a private message database. The
|
|
# database is gitignored, but three binary SQLite files reached the
|
|
# history anyway before this gate existed, so .gitignore is evidently
|
|
# not the whole defence. Exceptions go in .binary-allowlist.
|
|
# This repository is public and leaked once: real names, two phone
|
|
# numbers and a member's Signal uuid reached GitHub and only came off
|
|
# it by deleting the repository. The checker learns what to refuse from
|
|
# OUTSIDE the repo -- the photo pack, the message database, .env -- so
|
|
# the list of real names is never in the thing being checked.
|
|
- id: personal-data
|
|
name: no personal data
|
|
entry: scripts/check_personal_data.sh
|
|
language: system
|
|
types: [text]
|
|
|
|
# The same check on the commit message, because that is where two of
|
|
# the leaks actually lived.
|
|
- id: personal-data-message
|
|
name: no personal data in the commit message
|
|
entry: scripts/check_personal_data.sh --message
|
|
language: system
|
|
stages: [commit-msg]
|
|
always_run: true
|
|
pass_filenames: true
|
|
|
|
- id: no-binaries
|
|
name: no binary or image files
|
|
entry: scripts/check_no_binaries.sh
|
|
language: script
|
|
pass_filenames: true
|
|
|
|
# The shared 250-line cap. Checks only the files in the commit, so an
|
|
# unrelated commit never breaks. No baseline, no allowlist -- those are
|
|
# suppressions. The exemption list lives with the checker in ~/utils so
|
|
# this repo's idea of the rule cannot drift from every other repo's.
|
|
- id: file-length
|
|
name: file length <= 250 lines
|
|
entry: scripts/check_file_length.sh
|
|
language: system
|
|
|
|
# Four markdown namespaces (README / CLAUDE* / DOCS* / TODO*), plus the
|
|
# rule that a TODO carries "REMOVE ME AFTER FINISH" and nothing else may.
|
|
- id: md-naming
|
|
name: markdown naming convention
|
|
entry: scripts/check_md_naming.sh
|
|
language: system
|
|
files: \.md$
|
|
|
|
# Exact pins at newest stable. Runs repo-wide: a pin goes stale because
|
|
# upstream released, not because this commit touched the manifest.
|
|
- id: dependency-freshness
|
|
name: dependencies are newest stable
|
|
entry: scripts/check_dependency_freshness.sh --all
|
|
language: system
|
|
pass_filenames: false
|
|
always_run: true
|
|
|
|
- id: ruff-format
|
|
name: ruff format
|
|
entry: .venv/bin/ruff format --check
|
|
language: system
|
|
types_or: [python, pyi]
|
|
|
|
- id: ruff
|
|
name: ruff (ALL rules)
|
|
entry: .venv/bin/ruff check --force-exclude
|
|
language: system
|
|
types_or: [python, pyi]
|
|
|
|
- id: mypy
|
|
name: mypy --strict
|
|
entry: .venv/bin/mypy
|
|
language: system
|
|
pass_filenames: false
|
|
args: [signal_bot, fake_signal]
|
|
|
|
- id: shellcheck
|
|
name: shellcheck
|
|
# Same severity floor as CI: a stricter runner version must not be
|
|
# the thing that discovers a finding this hook let through.
|
|
entry: shellcheck -x --severity=style
|
|
language: system
|
|
types: [shell]
|
|
|
|
# The watchdog is bash, so pytest cannot reach it. Its own suite stubs
|
|
# curl and docker and drives every branch, including the give-up cap.
|
|
- id: watchdog-tests
|
|
name: watchdog behaviour (bash)
|
|
entry: scripts/test_watchdog.sh
|
|
language: system
|
|
pass_filenames: false
|
|
files: ^scripts/(signal_api_watchdog|test_watchdog)\.sh$
|
|
|
|
# 100% branch coverage, no omits. Runs on every commit: coverage is the
|
|
# whole point of the suite, and a partial run cannot prove it.
|
|
- id: pytest
|
|
name: pytest (100% branch coverage)
|
|
entry: .venv/bin/python -m pytest -q
|
|
language: system
|
|
pass_filenames: false
|
|
always_run: true
|