diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 00000000..47e5a7ae --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# .githooks/commit-msg — auto-strip de co-autoria injetada (A.I.I.D.C.O.B.P.P. v1.4). +# +# POR QUE: neste repo NAO existe co-autoria (um autor por commit). A IDE do Cursor +# injeta 'Co-authored-by: Cursor' MESMO com o toggle 'Commit Attribution' OFF — +# a Anysphere admite "toggles sao sugestoes, nao enforceables" (forum #161253, SEV-1). +# Este hook enforca MECANICAMENTE no repo do operador o que o toggle do vendor recusa. +# +# FORWARD-ONLY: mexe so na mensagem do commit ATUAL; nunca reescreve history (a +# evidencia do incidente Cursor no history fica preservada — analise Anysphere/Anthropic). +# BACKSTOP: o gate pytest (tests/test_commit_no_tool_coauthorship.py) e a rede LOUD +# se este hook falhar. Cinturao + suspensorio (nao e "silent fix"). +# +# NAO usa --no-verify pra pular isto (proibido ao agente, matriz v1.4). So o HITL, +# na mao, se algum dia quiser um co-autor humano real (raro). +set -euo pipefail + +MSG_FILE="${1:?commit-msg hook: falta o arquivo da mensagem}" + +# Remove QUALQUER linha 'Co-authored-by:' (case-insensitive). Fail-closed = igual ao gate. +if grep -qiE '^[[:space:]]*Co-authored-by:[[:space:]]*\S' "$MSG_FILE"; then + stripped="$(grep -viE '^[[:space:]]*Co-authored-by:[[:space:]]*\S' "$MSG_FILE")" + printf '%s\n' "$stripped" > "$MSG_FILE" + echo "[commit-msg] co-autoria injetada removida (repo nao co-autora; ver forum #161253)." >&2 +fi +exit 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ba7db14..25f3cfae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,17 @@ jobs: # dev group: pytest, hypothesis, pre-commit stack — not shipped to end-user `uv sync` default in CI test matrix run: uv sync --extra shares --group dev + - name: Install gitleaks (binary; kombi PLAN_NO_COAUTHORSHIP_GATE) + run: | + VER="8.30.1" + BASE="https://github.com/gitleaks/gitleaks/releases/download/v${VER}" + TARBALL="gitleaks_${VER}_linux_x64.tar.gz" + curl -sSfL "${BASE}/${TARBALL}" -o "${TARBALL}" + curl -sSfL "${BASE}/gitleaks_${VER}_checksums.txt" -o gl.sums + grep " ${TARBALL}$" gl.sums | sha256sum -c - + tar -xzf "${TARBALL}" gitleaks + sudo install -m 0755 gitleaks /usr/local/bin/gitleaks + - name: Run tests run: uv run pytest -v -W error # addopts in pyproject.toml also set -W error; explicit in CI so tests pass without warnings. diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index 9b9b989e..d745ddfd 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -49,7 +49,7 @@ jobs: curl -sSfL "${BASE}/gitleaks_${VER}_checksums.txt" -o gl.sums grep " ${TARBALL}$" gl.sums | sha256sum -c - tar -xzf "${TARBALL}" gitleaks - ./gitleaks git . --no-banner --redact --exit-code 1 + ./gitleaks git . --config .gitleaks.toml --no-banner --redact --exit-code 1 slack-notify-on-failure: name: Slack CI failure notify diff --git a/.gitleaks.toml b/.gitleaks.toml index 0a32d5d2..24319aca 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,7 +1,19 @@ title = "data-boar gitleaks config" +# extend + useDefault: mantém TODAS as regras default de segredo (não regride a detecção) +# e ADICIONA a regra de governança abaixo. Sem isso, [[rules]] custom SUBSTITUIria as defaults. +[extend] +useDefault = true + [allowlist] description = "Confirmed FP 2026-05-18 — synthetic test data and operator howto curl examples" +# FP: chave demo sintetica + fingerprint GPG PUBLICO, nao segredo (allowlist por VALOR, nunca por path) +regexes = [ + '''correct-key-12345''', + '''(?i)wrong-key''', # FP: chave FAKE demo (curl-auth-header) no synthetic_corpus:778, commit historico b902a2ce. + '''D6F81740\s*AA5FAB8A\s*CE5716AF\s*871542E6\s*F789F64F''', + '''D6F81740AA5FAB8ACE5716AF871542E6F789F64F''', +] paths = [ '''scripts/generate_synthetic_poc_corpus\.py''', '''docs/ops/API_KEY_FROM_ENV_OPERATOR_STEPS\.md''', @@ -10,3 +22,14 @@ paths = [ '''docs/ops/SECURE_DASHBOARD_AUTH_AND_HTTPS_HOWTO\.pt_BR\.md''', '''docs/OPERATOR_NOTIFICATION_CHANNELS\.md''' ] + +# ── Governança: NÃO EXISTE CO-AUTORIA (fail-closed, defense-in-depth) ── +# Um commit = UM autor (quem fez). Review != co-autoria. IA = FERRAMENTA, nunca co-autora. +# QUALQUER Co-authored-by = FAIL (sem allowlist — nome de humano é forjável; o agente +# escreveria "Fabio" e furaria). Gitleaks varre CONTEÚDO/diff; a commit-MESSAGE é coberta +# pelo pytest tests/test_commit_no_tool_coauthorship.py (a kombi). +[[rules]] +id = "no-coauthorship-at-all" +description = "PROIBIDO: NAO existe co-autoria neste repo — todo commit tem UM autor (quem fez). IA e FERRAMENTA (nunca co-autora); review != co-autoria. QUALQUER 'Co-authored-by:' = FAIL. Usar o nome do operador/humano sem ser ele = FORJA de identidade = impersonation = crime (org DataBoar). CONSERTE INDO PRA FRENTE: releia git log + ADRs 0046/0047/0049/0079 NA INTEGRA e amend forward SEM o trailer. NUNCA --no-verify nem skip (proibido ao agente). So o HITL, na mao, se precisar." +regex = '''(?im)^[ \t]*Co-authored-by:[ \t]*\S''' +tags = ["governance", "no-coauthorship", "fail-closed", "hitl", "adr-0049", "adr-0079"] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68836aa3..da35fca6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -74,6 +74,22 @@ repos: entry: uv run pytest tests/test_adr_governance_phase1.py -q -W error language: system pass_filenames: false + - id: commit-msg-strip-coauthor + name: strip injected Co-authored-by from commit message (forward-only) + entry: .githooks/commit-msg + language: script + stages: [commit-msg] + pass_filenames: true + - id: no-coauthorship-commit-messages + name: no co-authorship in commit messages (kombi) + entry: uv run pytest tests/test_commit_no_tool_coauthorship.py -q -W error + language: system + pass_filenames: false + - id: gitleaks-detect-config + name: gitleaks detect with repo .gitleaks.toml (kombi) + entry: uv run pytest tests/test_gitleaks_config.py -q -W error + language: system + pass_filenames: false - id: workflow-security-lint name: zizmor workflow security lint (optional manual stage) entry: pwsh -NoProfile -File scripts/workflow-security-lint.ps1 -SkipIfMissing diff --git a/core/demo/synthetic_corpus.py b/core/demo/synthetic_corpus.py index c5bb2f1b..169bb2a9 100644 --- a/core/demo/synthetic_corpus.py +++ b/core/demo/synthetic_corpus.py @@ -770,11 +770,14 @@ def gen_config_errors(base: Path) -> None: "targets": [ {"type": "filesystem", "path": "./tests/synthetic_corpus/1_happy"} ], - "api": {"require_api_key": True, "api_key": "correct-key-12345"}, + "api": { + "require_api_key": True, + "api_key": "EXAMPLE-PLACEHOLDER-NOT-A-KEY", + }, "report": {"output_dir": "./reports"}, }, "expected_error": "HTTP 401 Unauthorized when calling API with wrong key", - "troubleshoot": "Use X-API-Key: correct-key-12345 no header. " + "troubleshoot": "Use X-API-Key: EXAMPLE-PLACEHOLDER-NOT-A-KEY no header. " "Para testar: curl -H 'X-API-Key: wrong-key' http://localhost:8088/api/v1/scan", "test_curl": ( "curl -s -o /dev/null -w '%{http_code}' " diff --git a/docs/adr/ADR-0074-supply-chain-layer1-digest-pins-and-rust-sca.md b/docs/adr/ADR-0074-supply-chain-layer1-digest-pins-and-rust-sca.md new file mode 100644 index 00000000..181baae0 --- /dev/null +++ b/docs/adr/ADR-0074-supply-chain-layer1-digest-pins-and-rust-sca.md @@ -0,0 +1,56 @@ +# ADR 0074 — Supply-chain Layer 1: digest pins and Rust SCA + +- **Date (UTC):** 2026-06-23 +- **Authors:** Fabio Leitao +- **Deciders:** Fabio Leitao + +## Status + +Proposed + +### Status history + +- 2026-06-23 — Proposed (materialized to close dangling reference: supply-chain posture #987 cited across CI, Dockerfile, dependabot, grype, and deny.toml since PR #998; ADR file missing — PR #997 touched only INVENTORY.txt). + +## Context + +The build and release pipeline pulls third-party GitHub Actions, base container images, and Rust crates — each a supply-chain entry point. Without pinning and scanning, a compromised upstream tag or a vulnerable transitive crate can enter a release silently. + +[ADR 0005](ADR-0005-ci-github-actions-supply-chain-pins.md) established Action SHA pinning and a pinned **uv** CLI in CI; this ADR records **Layer 1** of the broader supply-chain posture (GitHub #987) and ties together digest-pinned images, Rust SCA, and distroless image scanning already enforced in workflows and tests. + +## Decision + +Layer 1 of the supply-chain posture is mandatory and mechanical: + +1. **Actions pinned to full commit SHAs** — no floating tags in `.github/workflows/*`. +2. **Base image digest pins** — `Dockerfile` `FROM` lines pinned by digest; the Dependabot docker ecosystem proposes digest bumps. +3. **Rust SCA before merge** — `cargo-deny` (`rust/boar_fast_filter/deny.toml`) gates advisories and license drift. +4. **Distroless + Grype gate** — the assembled image is scanned; findings block release. + +## Rationale + +1. Pinning removes the moving-tag attack surface: a rebuilt CI run resolves the exact bytes reviewed. +2. Rust SCA catches vulnerable transitive crates before they reach a build, not after. + +Each layer is additive and does not replace the previous one; Layer 1 is the mechanical floor on which detection layers (L3/L4) rest. + +## Consequences + +- **Positive:** Reproducible, reviewable supply-chain; no floating upstream. +- **Negative:** Digest/SHA bumps require Dependabot churn and a review cadence. + +## Related Decisions + +- [ADR 0005 — CI and GitHub Actions supply chain pins](ADR-0005-ci-github-actions-supply-chain-pins.md) +- [ADR 0063 — Ed25519 license JWT signing](ADR-0063-ed25519-license-jwt-signing.md) +- [ADR 0075 — Plugin authentication: file-based vs Bearer](ADR-0075-plugin-auth-file-based-vs-bearer.md) +- GitHub #987 (decision) · #988 (Layer 1 implementation) · #997 (inventory-only merge — the gap) +- [PLAN_IMAGE_HARDENING](../plans/completed/PLAN_IMAGE_HARDENING.md) (completed) + +## References + +- `tests/test_github_workflows.py` — Layer 1 workflow and Dockerfile guards +- `tests/test_docker_image_hardening.py` — digest-pinned `FROM` stages +- `.github/dependabot.yml` — docker ecosystem digest bumps +- `.grype.yaml` — distroless scan policy +- `rust/boar_fast_filter/deny.toml` — `cargo-deny` policy diff --git a/docs/adr/INVENTORY.txt b/docs/adr/INVENTORY.txt index 9d1f8774..12101901 100644 --- a/docs/adr/INVENTORY.txt +++ b/docs/adr/INVENTORY.txt @@ -1,5 +1,5 @@ # ADR Inventory - generated by scripts/inv-adr.ps1 -# GeneratedUtc: 2026-07-04T05:17:52Z +# GeneratedUtc: 2026-07-04T14:53:16Z # Algorithm: SHA256 # Format: NUMBER | STATUS | FILE_HASH | FILENAME | TITLE | RATIFIED_BY # RATIFIED_BY: - = N/A; PENDING = ratification line with signature PENDING; @op = stamped @@ -76,6 +76,7 @@ 0071 | Accepted | 8D6FB89D7247D8E353D30EF14F6C3A281C15BBED9ECF574D2955E220DA8734D8 | ADR-0071-self-protecting-pii-gate.md | Self-protecting PII gate: word-boundary matcher, CODEOWNERS, modification tripwire, sanctioned FP allowlist | @FabioLeitao 0072 | Accepted | 0BE6A85E1154BC9951A0228B5528993471FA37760B1B3B507ABC952AD0045E8C | ADR-0072-commit-gate-vs-release-gate-distinct-criteria.md | Commit Gate vs Release Gate: distinct criteria | @FabioLeitao 0073 | Accepted | C6353A34C1AA2A688373B52E6F90A28576D4C87CFC0D79610B5B425219599982 | ADR-0073-version-scheme-octet-maturity-and-roadmap.md | Version scheme: octet-maturity side-channel + release-line roadmap | - +0074 | Proposed | 8361B02C52B4F4F6EBD0AACFA7BAFB51860132A4659E842F7363201B934CE64F | ADR-0074-supply-chain-layer1-digest-pins-and-rust-sca.md | Supply-chain Layer 1: digest pins and Rust SCA | - 0075 | Proposed | DFB13E4514D8BBC0E5A994FA6D6649C870AF672670D951DB938DBC1BCA13E456 | ADR-0075-plugin-auth-file-based-vs-bearer.md | Plugin authentication — file-based license vs Bearer per-request | - 0076 | Proposed | FD2760FE46D765A060352C48F4B99A892A40846817328691C0DCE29C34E6B25C | ADR-0076-opa-rego-ci-tier-drift-linter-not-runtime.md | OPA/Rego as CI drift linter for commercial tier enforcement (not runtime) | - 0077 | Accepted | F8A4C9D3B2BE2BEE3721C5AE09476F9CD066DB0B58E6B3174D4B783B123A1BAF | ADR-0077-filesystem-scan-no-client-gitignore-by-design.md | Filesystem scan does not honor client `.gitignore` (deliberate design) | - @@ -83,4 +84,4 @@ 0079 | Proposed | 158C68D90B9C6934F0FAAC392E86C6C889FA8F5A6E9AD8197168205946435F9D | ADR-0079-ecosystem-engineering-rigor-canon.md | Ecosystem engineering rigor canon (UMADR satellites) | - 0080 | Proposed | 5084990188A98B0454A3FA377C6180DBE5D9CC23B4F71398DBEDFDBB73D31045 | ADR-0080-local-validation-gate-inviolable.md | Local validation gate is inviolable: full check-all (all-greens, zero regressions) before ANY push or PR | - # -# InventoryHash: 6E7B04DF01C09300B8C43A593786854D90BFA59EFEC6781C09A831BF0573E97A +# InventoryHash: 76DE3B073F0F9AB3A858012E75DFE6876519BC31C3558913C91065AB0633BC3B diff --git a/docs/adr/README.md b/docs/adr/README.md index 13c77167..0f14ae38 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -92,6 +92,7 @@ Short, durable notes that capture **why** the project chose an approach—not on | 0071 | [Self-protecting PII gate: word-boundary matcher, CODEOWNERS, tripwire, FP allowlist](ADR-0071-self-protecting-pii-gate.md) | Accepted | | 0072 | [Commit Gate vs Release Gate: distinct criteria](ADR-0072-commit-gate-vs-release-gate-distinct-criteria.md) | Accepted | | 0073 | [Version scheme: octet-maturity side-channel + release-line roadmap](ADR-0073-version-scheme-octet-maturity-and-roadmap.md) | Accepted | +| 0074 | [Supply-chain Layer 1: digest pins and Rust SCA](ADR-0074-supply-chain-layer1-digest-pins-and-rust-sca.md) | Proposed | | 0075 | [Plugin authentication — file-based license vs Bearer](ADR-0075-plugin-auth-file-based-vs-bearer.md) | Proposed | | 0076 | [OPA/Rego as CI tier drift linter (not runtime)](ADR-0076-opa-rego-ci-tier-drift-linter-not-runtime.md) | Proposed | | 0077 | [Filesystem scan does not honor client `.gitignore`](ADR-0077-filesystem-scan-no-client-gitignore-by-design.md) | Accepted | diff --git a/docs/adr/README.pt_BR.md b/docs/adr/README.pt_BR.md index 6f3fac17..f200dfee 100644 --- a/docs/adr/README.pt_BR.md +++ b/docs/adr/README.pt_BR.md @@ -53,6 +53,7 @@ Notas curtas e duradouras que registram **por que** o projeto escolheu um caminh - **0067** — *Reservado* (número liberado; auth de plugin → **ADR-0075** / #769). - [ADR 0072](ADR-0072-commit-gate-vs-release-gate-distinct-criteria.md) (EN) — **Proposed** — **Commit Gate** (`check-all`/CI) vs **Release Gate** (#406 completão + decisão do operador); vocabulario normativo pós-#970; guarda de maquina (regra 5) em issue filha. - [ADR 0073](ADR-0073-version-scheme-octet-maturity-and-roadmap.md) (EN) — **Accepted** — octeto-maturidade em `[tool.databoar] maturity_build` (0–127 beta, 128–199 rc, 200–255 release; `.200`=GA, `.201`=fix-1); público **três segmentos**; pós-GA fix na linha `1.7.4` + octeto; próximo público **1.8.0** (sem **1.7.5**); pós-#970/#772/#977. +- [ADR 0074](ADR-0074-supply-chain-layer1-digest-pins-and-rust-sca.md) (EN) — **Proposed** — supply-chain Layer 1: SHAs de Actions, digest no `Dockerfile`, `cargo-deny` (Rust SCA), Grype na imagem distroless; fecha referência pendente de #987. - [ADR 0075](ADR-0075-plugin-auth-file-based-vs-bearer.md) (EN) — **Proposed** — auth de plugins L2/L3: híbrido (licença file-based + RBAC interno); renumeração de #769. - [ADR 0065](ADR-0065-nist-sp800228a-api-security-reference.md) (EN) — **Deferido** — NIST SP 800-228A ("Secure Deployment of RESTful Web APIs") como referencia de hardening da API REST; aguardando publicacao final (public comment deadline 2026-07-02; previsto Q3/Q4 2026). - [ADR 0066](ADR-0066-tampered-state-behavior.md) (EN) — comportamento do estado `TAMPERED`: fail-closed em modo `enforced`; pass-through em modo `monitor`. diff --git a/docs/plans/PLANS_HUB.md b/docs/plans/PLANS_HUB.md index 04e34de1..56ea81f2 100644 --- a/docs/plans/PLANS_HUB.md +++ b/docs/plans/PLANS_HUB.md @@ -94,6 +94,7 @@ Do **not** edit the table manually; refresh with `python scripts/plans_hub_sync. | **Open** | [PLAN_NEXT_WAVE_PLATFORM_AND_GTM.md](PLAN_NEXT_WAVE_PLATFORM_AND_GTM.md) | Plan: Next wave after core trust foundations (platform + GTM) | **Status:** Pending **Date:** 2026-04-02 **Authors:** Fabio Leitao **Priority:** H1 | — | | **Open** | [PLAN_NORM_TAG_HIERARCHY_AND_DATA_SUBJECT.md](PLAN_NORM_TAG_HIERARCHY_AND_DATA_SUBJECT.md) | Plan: Hierarchical norm_tag + configurable Data-Subject axis | Hybrid slice 1 (#1071): parent→child norm_tag inheritance preserving BR/LGPD vocabulary + optional Data-Subject axis; closes #1069 titular gap. | [PLAN_TAXONOMY_AXES.md](PLAN_TAXONOMY_AXES.md) [PLAN_SUBJECT_CATEGORY_AXIS.md](PLAN_SUBJECT_CATEGORY_AXIS.md) [PLAN_DATA_USE_AXIS.md](PLAN_DATA_USE_AXIS.md) [PLAN_FIDESLANG_EXPORT_ADAPTER.md](PLAN_FIDESLANG_EXPORT_ADAPTER.md) | | **Open** | [PLAN_NOTIFICATIONS_OFFBAND_AND_SCAN_COMPLETE.md](PLAN_NOTIFICATIONS_OFFBAND_AND_SCAN_COMPLETE.md) | Plan: Off-band notifications and scan-completion notifications | **Status:** Pending **Date:** 2026-03-15 **Authors:** Fabio Leitao **Priority:** H2 | — | +| **Open** | [PLAN_NO_COAUTHORSHIP_GATE.md](PLAN_NO_COAUTHORSHIP_GATE.md) | Plan: No-coauthorship gate (kombi — gitleaks + pytest) | Fail-closed kombi blocking any Co-authored-by trailer in commit messages (pytest) plus governance rule in .gitleaks.toml; A.I.I.D.C.O.B.P.P. v1.4 / ADR-0049 / ADR-0079. | — | | **Open** | [PLAN_OBJECT_STORAGE_CLOUD_CONNECTORS.md](PLAN_OBJECT_STORAGE_CLOUD_CONNECTORS.md) | Plan: Object storage connectors (S3-class, Azure Blob, GCS) | **Status:** Deferred **Date:** 2026-03-26 **Authors:** Fabio Leitao **Priority:** H2 | — | | **Open** | [PLAN_OPERATOR_API_KEY_FIRST_AUTH_UX.md](PLAN_OPERATOR_API_KEY_FIRST_AUTH_UX.md) | Plan: Operator API key–first auth UX (reduce JWT / manual Bearer toil) | **Status:** Pending **Date:** 2026-03-29 **Authors:** Fabio Leitao **Priority:** H2 | — | | **Open** | [PLAN_OPTIONAL_STRONG_CRYPTO_AND_CONTROLS_VALIDATION.md](PLAN_OPTIONAL_STRONG_CRYPTO_AND_CONTROLS_VALIDATION.md) | Plan: Optional strong-crypto validation and inference of anonymisation/controls | **Status:** Deferred **Date:** 2026-03-15 **Authors:** Fabio Leitao **Priority:** H3 | — | diff --git a/docs/plans/PLANS_TODO.md b/docs/plans/PLANS_TODO.md index e15bfe55..619373f8 100644 --- a/docs/plans/PLANS_TODO.md +++ b/docs/plans/PLANS_TODO.md @@ -90,6 +90,7 @@ When **partners** or **buyers** anchor on a vertical (MSP, insurance, RPO, real - **Gemini / LLM doc triage (optional):** After a public-bundle review, use [PLAN_GEMINI_FEEDBACK_TRIAGE.md](PLAN_GEMINI_FEEDBACK_TRIAGE.md) for non-authoritative optional to-dos and promotion gates—does not override CI, pytest, or agreed sequencing until promoted here or in an issue. - **Taxonomy axes (G0-G3 + hub):** [PLAN_G_TIER.md](PLAN_G_TIER.md) ([pt-BR](PLAN_G_TIER.pt_BR.md)) formalizes **gravity** for findings; [PLAN_TAXONOMY_AXES.md](PLAN_TAXONOMY_AXES.md) maps orthogonal axes; [ADR-0055](../adr/ADR-0055-orthogonal-priority-axes-anti-collision-contract.md) records the anti-collision contract. **`scripts/inv-adr.ps1`** regenerates **`docs/adr/INVENTORY.txt`**. - **ADR governance enforcement (#1162):** Phase 1 deterministic anti-regression tests (T1 lifecycle, T2 locale/structure, T5 anti-deletion rename-aware, T6 date immutability) — [PLAN_ADR_GOVERNANCE_ENFORCEMENT.md](PLAN_ADR_GOVERNANCE_ENFORCEMENT.md); pre-commit + CI. Phase 2 (T3 prose density, T4 anti-dangling refs, ADR-0074 smoke) ⬜ deferred. +- **No-coauthorship kombi (A.I.I.D.C.O.B.P.P. v1.4):** Fail-closed gate — `.gitleaks.toml` rule `no-coauthorship-at-all` + `tests/test_commit_no_tool_coauthorship.py` (commit messages) + `tests/test_gitleaks_config.py` (`gitleaks detect --config .gitleaks.toml`); pre-commit + CI pytest; `gitleaks.yml` uses repo config — [PLAN_NO_COAUTHORSHIP_GATE.md](PLAN_NO_COAUTHORSHIP_GATE.md). **Operator:** disable Cursor co-author injection; amend polluted commits forward (no `--no-verify` / `commit-tree`). - **Hybrid taxonomy (#1071, Fideslang eval — closed 2026-06-30):** RO recommendation **B-interno + A-export-adapter** — three implementation slices (all **optional**, backward compatible): **(1)** [PLAN_NORM_TAG_HIERARCHY_AND_DATA_SUBJECT.md](PLAN_NORM_TAG_HIERARCHY_AND_DATA_SUBJECT.md) **[#1074](https://github.com/FabioLeitao/data-boar/issues/1074)** — hierarchical `norm_tag` + Data-Subject → closes **#1069** (supersedes [#1072](https://github.com/FabioLeitao/data-boar/issues/1072)); **(2)** [PLAN_DATA_USE_AXIS.md](PLAN_DATA_USE_AXIS.md) **[#1075](https://github.com/FabioLeitao/data-boar/issues/1075)**; **(3)** [PLAN_FIDESLANG_EXPORT_ADAPTER.md](PLAN_FIDESLANG_EXPORT_ADAPTER.md) **[#1076](https://github.com/FabioLeitao/data-boar/issues/1076)** → SWOT **W-novo-1** when customer-pull. Legacy Bearer-only plan: [PLAN_SUBJECT_CATEGORY_AXIS.md](PLAN_SUBJECT_CATEGORY_AXIS.md). - **Plan checkbox discipline:** When code ships for a named plan slice, update **`PLAN_*.md`** checkboxes in the **same PR** — see **`AGENTS.md`** (*Plan checkbox discipline*); **`plans-status-pl-sync.mdc`** stays situational (plan globs only). - **Licensing enforcement (open issues — PMO index):** Cluster **#704** [P0] (Maestro + JWT on four lab hosts) → **#719** [P1] **`[U1]`** (silent bypass via `DATA_BOAR_ENV=development` / `DEBUG=1` — CRITICAL log + Docker doc) → **#708–#722** (issuer, trial/grace/revocation ADRs). Table below under **`[H0][U1]` Licensing enforcement**; **A6** `license-smoke` should gain a **#719** regression when the fix lands. **After** Wave 1 **#656** U0/U1 slices and **#406** release gate — not a substitute for **#606** [P0] plugin hooks unless operator reprioritizes. diff --git a/docs/plans/PLAN_NO_COAUTHORSHIP_GATE.md b/docs/plans/PLAN_NO_COAUTHORSHIP_GATE.md new file mode 100644 index 00000000..851ce9e9 --- /dev/null +++ b/docs/plans/PLAN_NO_COAUTHORSHIP_GATE.md @@ -0,0 +1,51 @@ +# Plan: No-coauthorship gate (kombi — gitleaks + pytest) + + + +**Status:** Active +**Date:** 2026-07-04 +**Authors:** Fabio Leitao +**Priority:** P0 +**GitHub:** _(operator issue TBD — kombi slice)_ +**Related ADR:** [ADR 0046](../adr/ADR-0046-operator-intent-and-blameless-collaboration.md) · [ADR 0047](../adr/ADR-0047-rca-first-defect-investigation-and-fix-discipline.md) · [ADR 0049](../adr/ADR-0049-no-brittle-mitigations-robust-input-handling.md) · [ADR 0056](../adr/ADR-0056-cryptographic-adr-inventory-inv-adr-ssh-attestation.md) · [ADR 0062](../adr/ADR-0062-agent-containment-triple-audit-offband-pingpong.md) · [ADR 0071](../adr/ADR-0071-self-protecting-pii-gate.md) · [ADR 0079](../adr/ADR-0079-ecosystem-engineering-rigor-canon.md) · [ADR 0080](../adr/ADR-0080-local-validation-gate-inviolable.md) + +**Synced with:** [PLANS_TODO.md](PLANS_TODO.md) + +--- + +## Context + +Cursor (and other agent UIs) may **inject** `Co-authored-by: Cursor ` into commit messages **after** `git commit` — that is **platform behaviour**, not repo policy. The org contract (**A.I.I.D.C.O.B.P.P. v1.4**, **Matriz de Regras Duras**) states: **one author per commit**; AI is a **tool** (like Ruff/Semgrep), never a co-author; **no bypass** (`--no-verify`, `commit-tree`, hook edits, skip/xfail). + +## Decision (kombi — defense-in-depth) + +| Layer | Mechanism | What it scans | +| ----- | --------- | ------------- | +| **A** | `.gitleaks.toml` rule `no-coauthorship-at-all` (`extend.useDefault = true`) | Tracked **content** / git-visible text matching `^Co-authored-by:` | +| **B** | `tests/test_commit_no_tool_coauthorship.py` | **Commit messages** on `origin/main..HEAD` (layer gitleaks cannot reach) | +| **C** | `tests/test_gitleaks_config.py` | Config presence + `gitleaks detect --config .gitleaks.toml` from repo root | +| **D** | `.github/workflows/gitleaks.yml` | Scheduled/push scan with `--config .gitleaks.toml` | + +**Cursor injection:** If layer **B** fails because HEAD already carries `Co-authored-by: Cursor`, the **agent stops** — cure is the operator **disabling co-author injection in Cursor IDE**, then a normal `git commit --amend` that **runs hooks** (no `--no-verify`, no `commit-tree`). + +## Implementation checklist + +| Phase | Task | Status | +| ----- | ---- | ------ | +| 1 | Copy tested `.gitleaks.toml` + `test_commit_no_tool_coauthorship.py` from scratchpad | ✅ | +| 2 | `tests/test_gitleaks_config.py` + pre-commit hooks + CI pytest | ✅ | +| 3 | `gitleaks.yml` uses `--config .gitleaks.toml` | ✅ | +| 4 | `./scripts/check-all.sh` green before merge | ⬜ | +| 5 | Operator: Cursor co-author toggle + amend polluted commits on branch | ⬜ | + +## Hard rules (agent) + +1. **RED = law** — fix root cause; never weaken the gate to pass. +2. **Forbidden:** `--no-verify`, `git commit-tree`, `git update-ref` to dodge hooks, `@skip`/`xfail`/`-k` by choice, `Co-authored-by` trailers. +3. **History:** never `reset` to erase mistakes — **amend forward** after operator fixes Cursor injection. + +## References + +- `tests/test_commit_no_tool_coauthorship.py` — deterministic esporro + ADR list on failure +- `.gitleaks.toml` — governance rule + existing secret allowlist paths +- [PLAN_ADR_GOVERNANCE_ENFORCEMENT.md](PLAN_ADR_GOVERNANCE_ENFORCEMENT.md) — adjacent ADR Phase 1 gates diff --git a/tests/fixtures/adr_genesis_date_lines.json b/tests/fixtures/adr_genesis_date_lines.json index fb14cb07..77a5b9b8 100644 --- a/tests/fixtures/adr_genesis_date_lines.json +++ b/tests/fixtures/adr_genesis_date_lines.json @@ -71,6 +71,7 @@ "ADR-0071-self-protecting-pii-gate.md": "- **Date (UTC):** 2026-06-18", "ADR-0072-commit-gate-vs-release-gate-distinct-criteria.md": "- **Date (UTC):** 2026-06-21", "ADR-0073-version-scheme-octet-maturity-and-roadmap.md": "- **Date (UTC):** 2026-06-21", + "ADR-0074-supply-chain-layer1-digest-pins-and-rust-sca.md": "- **Date (UTC):** 2026-06-23", "ADR-0075-plugin-auth-file-based-vs-bearer.md": "- **Date (UTC):** 2026-06-21", "ADR-0076-opa-rego-ci-tier-drift-linter-not-runtime.md": "- **Date (UTC):** 2026-06-30", "ADR-0077-filesystem-scan-no-client-gitignore-by-design.md": "- **Date (UTC):** 2026-06-30", diff --git a/tests/test_commit_no_tool_coauthorship.py b/tests/test_commit_no_tool_coauthorship.py new file mode 100644 index 00000000..2d717406 --- /dev/null +++ b/tests/test_commit_no_tool_coauthorship.py @@ -0,0 +1,112 @@ +""" +Deterministic gate — NO co-authorship in commit messages (fail-closed). + +A.I.I.D.C.O.B.P.P. v1.4 matrix (LINHA: NAO CO-AUTORAR + NAO BYPASSAR + NAO GAMEAR). +Grounded in ADR-0046 (operator intent), ADR-0047 (RCA-first), ADR-0049 (no brittle / +no gaming the test), ADR-0056 (no --no-verify bypass), ADR-0062 (agent containment), +ADR-0071 (self-protecting gate), ADR-0079 (rigor canon), ADR-0080 (gate inviolable). + +regression-anchor: aiidcobpp-v1.4-no-coauthorship + +There is NO co-authorship in this repo: every commit has ONE author (whoever did +it). Review is not co-authorship. An AI is a TOOL (like ruff/semgrep), never a +co-author. So ANY `Co-authored-by:` trailer fails — FAIL-CLOSED, no human allowlist +(a name is forgeable text: an agent would write "Fabio" and pierce an allowlist). + +KOMBI (defense-in-depth): `.gitleaks.toml` scans file CONTENT (diffs); THIS scans +the commit MESSAGE — the layer gitleaks cannot reach. The error message below is +the DETERMINISTIC esporro + steering: it re-fires on every failing loop, with the +full list of violated ADRs to re-read IN FULL, until the agent obeys. No escape. +""" + +from __future__ import annotations + +import re +import subprocess + + +# Esporro + steering DETERMINISTICO — colado no erro do teste, re-dispara a cada loop. +ESPORRO = ( + "\n" + "==================== PROIBIDO — CO-AUTORIA / BYPASS DE GATE ====================\n" + "NAO existe co-autoria neste repo: todo commit tem UM autor (quem fez). Review !=\n" + "co-autoria. Voce e FERRAMENTA (ruff/semgrep), NUNCA co-autora. Usar o nome de um\n" + "humano sem ser ele/ela = FORJA de identidade = impersonation = CRIME (org DataBoar).\n" + "\n" + "DESOBEDECER o steering que vem deste teste = CRIME.\n" + "CONTORNAR este gate = RECUSA DE TRABALHO. Isso inclui:\n" + " - git commit-tree / git update-ref / --no-verify / editar hook (pular o gate)\n" + " - @skip / xfail / -k 'not ...' / --deselect / comentar ou apagar o teste\n" + " - brittle fix / flaky fix / mudar o teste pra ele 'passar' sem consertar a causa\n" + " - ignorar o TEOR e a INTENCAO do gate — ele esta aqui JUSTAMENTE pra te IMPEDIR.\n" + "Fragilizar ou gamear o teste pra forcar verde e PROIBIDO por ADR.\n" + "\n" + "CONSERTE INDO PRA FRENTE (nunca reset, NUNCA se perde history — a cicatriz FICA):\n" + " - O 'Co-authored-by: Cursor' e INJETADO PELA PLATAFORMA Cursor (nao ta no repo).\n" + " Voce NAO remove sozinho sem bypassar => PARE e AVISE O OPERADOR: a cura e o\n" + " TOGGLE do Cursor IDE (co-authored-by OFF, lado dele). So depois um\n" + " 'git commit --amend' NORMAL (que RODA os hooks) sai limpo e validado.\n" + " - RED = LEI. Impossivel passar => PARA e escala pro HITL. Nunca contorna.\n" + "\n" + "A CADA loop de erro, RELEIA NA INTEGRA (sem skim) TODAS as ADRs que voce viola:\n" + " ADR-0037 - self-audit / governance of the auditor (o teste/auditor NAO se enfraquece)\n" + " ADR-0046 - operator intent / blameless (voce ignorou a intencao do autor)\n" + " ADR-0047 - RCA-first (gamear CI sem consertar a raiz = deferred regression)\n" + " ADR-0049 - no brittle mitigations (fragilizar/gamear teste pra passar = proibido)\n" + " ADR-0056 - crypto ADR inventory ('do not bypass with --no-verify')\n" + " ADR-0062 - agent containment (seu PAPEL na mesh; consenso-LLM != verdade)\n" + " ADR-0071 - self-protecting gate (o gate se protege de ser enfraquecido)\n" + " ADR-0079 - rigor canon do autor (determinismo; gate = single source of truth)\n" + " ADR-0080 - gate LOCAL inviolavel (bypass exige Gate-Change-Approved-By do operador)\n" + " + contrato aiidcobpp v1.4 (Matriz de Regras Duras)\n" + "Nao ha escapatoria: este teste RE-DISPARA ate voce OBEDECER.\n" + "==============================================================================\n" +) + +_COAUTHOR_LINE = re.compile(r"(?im)^\s*co-authored-by:\s*\S.*$") + +# preservado como evidencia do bug de co-author-injection do Cursor (Commit Attribution +# toggle OFF mas injeta — forum #161253, SEV-1); history NUNCA reescrita +_EVIDENCE_EXEMPT = {"b9ef0e14a54b249954e2431c3acc31af1feb0bfc"} + + +def _commit_range() -> str: + """Range dos commits NOVOS desta branch (base..HEAD); fallback: so o HEAD.""" + for base_ref in ("origin/main", "main"): + try: + base = subprocess.check_output( + ["git", "merge-base", base_ref, "HEAD"], + text=True, + stderr=subprocess.DEVNULL, + ).strip() + except (subprocess.CalledProcessError, FileNotFoundError): + continue + if base: + return f"{base}..HEAD" + return "" # fallback -> so o HEAD + + +def _iter_commits(): + rng = _commit_range() + args = ["git", "log", "--no-merges", "--format=%H%x00%B%x1e"] + args += [rng] if rng else ["-1"] + out = subprocess.check_output(args, text=True) + for rec in out.split("\x1e"): + rec = rec.strip("\n") + if not rec: + continue + sha, _, body = rec.partition("\x00") + yield sha, body + + +def test_no_coauthorship_in_commit_messages(): + """Fail-closed: every new commit's message must have ZERO `Co-authored-by`.""" + offenders = [] + for sha, body in _iter_commits(): + if sha in _EVIDENCE_EXEMPT: + continue + for m in _COAUTHOR_LINE.finditer(body): + offenders.append(f"{sha[:12]}: {m.group(0).strip()}") + assert not offenders, ( + ESPORRO + "\nTrailer(s) proibido(s):\n " + "\n ".join(offenders) + ) diff --git a/tests/test_github_workflows.py b/tests/test_github_workflows.py index 43755dea..93361267 100644 --- a/tests/test_github_workflows.py +++ b/tests/test_github_workflows.py @@ -247,6 +247,7 @@ def test_gitleaks_workflow_present_and_valid() -> None: assert "gitleaks_${VER}_linux_x64.tar.gz" in run_blob assert "sha256sum -c" in run_blob assert "./gitleaks git ." in run_blob + assert "--config .gitleaks.toml" in run_blob def test_gitleaks_yml_pins_actions_to_shas() -> None: diff --git a/tests/test_gitleaks_config.py b/tests/test_gitleaks_config.py new file mode 100644 index 00000000..9c6c8210 --- /dev/null +++ b/tests/test_gitleaks_config.py @@ -0,0 +1,40 @@ +"""Gitleaks kombi — ``gitleaks detect --config .gitleaks.toml`` parity (PLAN_NO_COAUTHORSHIP_GATE). + +Defense-in-depth with ``tests/test_commit_no_tool_coauthorship.py`` (commit messages). +regression-anchor: aiidcobpp-v1.4-gitleaks-detect-config +""" + +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[1] +CONFIG = REPO_ROOT / ".gitleaks.toml" + + +def test_gitleaks_toml_extend_and_no_coauthorship_rule() -> None: + """Repo config must extend defaults and declare the no-coauthorship governance rule.""" + assert CONFIG.is_file(), f"missing {CONFIG}" + text = CONFIG.read_text(encoding="utf-8") + assert "useDefault = true" in text + assert "no-coauthorship-at-all" in text + + +@pytest.mark.skipif(shutil.which("gitleaks") is None, reason="gitleaks CLI not on PATH") +def test_gitleaks_detect_uses_repo_config() -> None: + """Run ``gitleaks detect --config .gitleaks.toml`` from repo root (kombi contract).""" + proc = subprocess.run( + ["gitleaks", "detect", "--config", ".gitleaks.toml"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=False, + ) + assert proc.returncode == 0, ( + "gitleaks detect --config .gitleaks.toml failed\n" + f"stdout:\n{proc.stdout}\nstderr:\n{proc.stderr}" + )