bug_bunny 5.1.0 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +20 -6
- data/docs/errors/errors.md +12 -6
- data/docs/glossary/glossary.md +2 -2
- data/docs/release/release.md +22 -14
- data/lib/bug_bunny/exception.rb +9 -5
- data/lib/bug_bunny/observability.rb +83 -3
- data/lib/bug_bunny/version.rb +1 -1
- data/skill/SKILL.md +17 -6
- data/skill/references/errores.md +7 -3
- data/spec/unit/observability_spec.rb +158 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd173a6f940a79457235d63629891426cba1f94a8381c19a219e38f275fe96dd
|
|
4
|
+
data.tar.gz: 78f927d9ed2ef4d04bf5b3059a3b3d54b93974ee11a8d7235ade9ae82095a51b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 188ad45954c08cf19dda7625236fdc8da934676de77a6af7136547893f2fc8ae0ad782aa890782dbd59e16b39b90d8fa8dec3f9b1c9a5c3b4b4fd22db5c77f8f
|
|
7
|
+
data.tar.gz: 87bf5385a97fea06f86fccb396ade8ce2602ec831181e4a2ba7893711a784eccb4d922ca4303137f0e25f7a625d88d015e61858f4f48209fa89624ae02a92cea
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.1.1] - 2026-07-31
|
|
4
|
+
|
|
5
|
+
### Correcciones
|
|
6
|
+
- **`safe_log` redacta credenciales embebidas en el VALOR, no solo por nombre de clave (#61):** el filtro por-clave solo ve el NOMBRE, así que una credencial en texto libre pasaba entera al log. El caso canónico es el `message` de una excepción inesperada, que llega como `reason=`/`error_message=` —nombres no sensibles— y puede arrastrar `Authorization: "Bearer …"`. Se agrega `Observability.redact_value`, aplicada a todo valor no numérico que `safe_log` serializa, con tres reglas: esquemas de auth HTTP (`Bearer`/`Basic`), key sensible dentro del texto (conserva el nombre de la key y filtra solo el valor) y credenciales en URL (`user:pass@host`). Cubre las variantes con separador y camelCase (`access_token=`, `user_password=`, `accessToken=`), igual que el matching por substring de `sensitive_key?`. No-breaking: no cambia firmas ni el formato `k=v`, y un log sin credenciales sale idéntico. Riesgo asumido: sobre-redacción de contenido diagnóstico anclado a marcadores de credencial (un `token=missing` sale filtrado). — @gedera
|
|
7
|
+
- **Un valor `Hash` sigue siendo JSON parseable después de redactar:** se agrega `Observability.redact_structure`, que redacta la estructura **antes** de serializar (recorre `Hash` anidado y `Array`) en vez de aplicar el regex sobre el JSON ya serializado, que colapsaba el par `"token": "abc"` y dejaba el campo sin parsear — el secreto desaparecía, pero quien consume el log perdía el objeto entero. De paso, las keys internas del `Hash` ahora también pasan por `sensitive_key?`. — @gedera
|
|
8
|
+
|
|
9
|
+
### Documentación
|
|
10
|
+
- La lista de claves sensibles **deja de replicarse**: el docblock de `Error#raw_response` y `docs/errors/` apuntan a `Observability::SENSITIVE_KEYS` como canónica, en vez de duplicarla. La copia venía desincronizada (incluía `pass` bare, que está excluido a propósito para no filtrar `passport_number`, y le faltaban `authorization`, `credential`, `private_key`, `csrf`, `session_id`). `docs/glossary/`, README y `skill/` describen ahora las dos capas de redacción. — @Pslp
|
|
11
|
+
|
|
3
12
|
## [5.1.0] - 2026-07-22
|
|
4
13
|
|
|
5
14
|
### Nuevas funcionalidades
|
data/README.md
CHANGED
|
@@ -365,7 +365,10 @@ BugBunny measures and emits durations automatically — **there is no need to wr
|
|
|
365
365
|
| `consumer.message_processed` | `duration_s` | Message processing (router + controller + reply). |
|
|
366
366
|
| `consumer.execution_error` | `duration_s` | Elapsed time until the error. |
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
Credentials are redacted to `[FILTERED]` across all log output, in two layers:
|
|
369
|
+
|
|
370
|
+
- **By key name** — sensitive keys (`password`, `token`, `secret`, `api_key`, `authorization`, etc.) are matched as substrings, so variants like `user_password` or `accessToken` are covered too.
|
|
371
|
+
- **By value content** — a credential embedded in free text is redacted even when the key name is *not* sensitive. The canonical case: an unexpected exception whose `message` carries `Authorization: "Bearer …"` and reaches the log as `reason=`. Covers HTTP auth schemes, a sensitive key inside the text, and credentials in a URL. `Hash` values are redacted **before** being serialized, so the field stays valid JSON.
|
|
369
372
|
|
|
370
373
|
---
|
|
371
374
|
|
|
@@ -429,20 +432,31 @@ end
|
|
|
429
432
|
|
|
430
433
|
Artefactos de detalle (modelo `dev-*`, RFC-001). El README indexa; no duplica.
|
|
431
434
|
|
|
435
|
+
Anclado a `v5.1.1`.
|
|
436
|
+
|
|
432
437
|
| Capa | Artefacto | Estado |
|
|
433
438
|
|---|---|---|
|
|
434
|
-
| Datos | — | n/a — gema sin DB (sin schema/models) |
|
|
435
439
|
| Glosario | [docs/glossary/glossary.md](docs/glossary/glossary.md) | parcial, acreta por PR |
|
|
436
440
|
| Comportamiento | [docs/behavior/behavior.md](docs/behavior/behavior.md) | completa — 6 flujos (backfill on-demand) |
|
|
437
|
-
|
|
|
441
|
+
| Configuración | [docs/config/configuracion.md](docs/config/configuracion.md) | §a-§e/§i estructura + §f/§g/§h enrich — completa |
|
|
442
|
+
| Dependencias consumidas | [docs/consumed/rabbitmq.md](docs/consumed/rabbitmq.md) | §a/§b/§d estructura + §c/§e enrich |
|
|
443
|
+
| Errores | [docs/errors/errors.md](docs/errors/errors.md) | §a/§b/§d completas; §c política inferida (verificación humana pendiente) |
|
|
444
|
+
| Test | [docs/test/testing.md](docs/test/testing.md) | §a-§h — 16 unit / 7 integration |
|
|
445
|
+
| Release | [docs/release/release.md](docs/release/release.md) | completa (régimen gema: build→publish; §e/f/g n/a) |
|
|
446
|
+
| Datos | — | n/a — gema sin DB (sin schema/models) |
|
|
447
|
+
| Eventos | — | n/a — la gema **es** el transporte; no declara catálogo de eventos de dominio propio |
|
|
448
|
+
| Seguridad | — | n/a — sin authn/authz propias; el guard anti-RCE (403) se documenta en `docs/errors/` |
|
|
449
|
+
| Operaciones / Interfaz / Topología | — | pendiente — ver nota |
|
|
438
450
|
|
|
439
|
-
**Coexistencia transitoria con destino pendiente (RFC-008 §2 — interim de migración):** mientras
|
|
451
|
+
**Coexistencia transitoria con destino pendiente (RFC-008 §2 — interim de migración):** mientras las capas `docs/api/` (operaciones), `docs/interface/` y `docs/topology/` no estén generadas para este repo, el contrato que les correspondería permanece embebido:
|
|
440
452
|
|
|
441
|
-
- **En este README:**
|
|
453
|
+
- **En este README:** la jerarquía de excepciones, la API de configuración y los modos de entrega.
|
|
442
454
|
- **En `skill/SKILL.md`:** además el diagrama de arquitectura (flujo RPC).
|
|
443
455
|
- **Guías how-to** (`skill/references/*.md`, pre-estándar): el README las enlaza pese a la regla "no referenciar `skill/` desde el README" — destino futuro `docs/howto/`.
|
|
444
456
|
|
|
445
|
-
Por RFC-008 §2: no se fabrica la capa, no se borra contrato sin destino, no se duplica; migra cuando
|
|
457
|
+
Por RFC-008 §2: no se fabrica la capa, no se borra contrato sin destino, no se duplica; migra cuando se generen, mismo PR. Estado transitorio declarado, no excepción permanente. Origen del gap (resuelto, normado): [sequre/ai_knowledge#95](https://github.com/sequre/ai_knowledge/issues/95).
|
|
458
|
+
|
|
459
|
+
> **Nota de alcance (2026-07-31):** la justificación original de este interim era que el generador no implementaba esas capas. Ya las implementa, así que el pendiente es de **este repo**, no de tooling — en particular `docs/interface/` (RFC-004), que es donde correspondería registrar la API pública de la gema. Queda como trabajo propio, fuera del alcance de este release.
|
|
446
460
|
|
|
447
461
|
How-to (pre-estándar):
|
|
448
462
|
[Routing](skill/references/routing.md) ·
|
data/docs/errors/errors.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Errores — bug_bunny
|
|
2
2
|
|
|
3
3
|
> meta: artefacto errores · RFC-020 · generado `arch-structure` (§a/§b/§d) +
|
|
4
|
-
> `arch-enrich` (§c) · anclado a `
|
|
5
|
-
> `remote_error.rb`, `middleware/raise_error.rb`, `controller.rb`, `consumer.rb
|
|
6
|
-
> · fecha 2026-
|
|
7
|
-
> completa (enrich, **inferida** de HTTP/AMQP —
|
|
4
|
+
> `arch-enrich` (§c) · anclado a `5697541`, `lib/bug_bunny/exception.rb`,
|
|
5
|
+
> `remote_error.rb`, `middleware/raise_error.rb`, `controller.rb`, `consumer.rb`,
|
|
6
|
+
> `observability.rb` · fecha 2026-07-31 · cobertura: §a/§b/§d completas
|
|
7
|
+
> (estructura); §c política completa (enrich, **inferida** de HTTP/AMQP —
|
|
8
|
+
> verificación humana pendiente).
|
|
8
9
|
|
|
9
10
|
## 1. Resumen
|
|
10
11
|
|
|
@@ -156,8 +157,13 @@ parsea el body, devuelve `parsed['errors']` por convención o el cuerpo completo
|
|
|
156
157
|
|
|
157
158
|
> **Seguridad (cruza RFC-017):** `raw_response` puede contener datos sensibles en
|
|
158
159
|
> `details`. La gema lo entrega crudo a propósito; **sanitizar antes de cualquier
|
|
159
|
-
> sink** (Sentry/logs)
|
|
160
|
-
>
|
|
160
|
+
> sink** (Sentry/logs) es responsabilidad del consumidor (`exception.rb:25-34`).
|
|
161
|
+
> La lista canónica de claves sensibles es `Observability::SENSITIVE_KEYS`
|
|
162
|
+
> (`observability.rb:13-16`) y **no se replica acá** — replicarla la desincroniza
|
|
163
|
+
> (ojo: `pass` bare NO está en la lista, a propósito, para no filtrar
|
|
164
|
+
> `passport_number`). Para sanear se reusa `Observability.sensitive_key?` (filtra
|
|
165
|
+
> por NOMBRE de clave) y `Observability.redact_structure` (recorre la estructura
|
|
166
|
+
> y además redacta credenciales embebidas en el VALOR → `[FILTERED]`).
|
|
161
167
|
|
|
162
168
|
## 3. Inferencias
|
|
163
169
|
|
data/docs/glossary/glossary.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Glosario — bug_bunny
|
|
2
2
|
|
|
3
|
-
> meta: artefacto `glosario` · RFC-009 (binding opcional, r: §2 materialización no-tabular) · generado dev-enrich (siembra) · anclado a `
|
|
3
|
+
> meta: artefacto `glosario` · RFC-009 (binding opcional, r: §2 materialización no-tabular) · generado dev-enrich (siembra) · anclado a `5697541` · fecha 2026-07-31 · cobertura: parcial, acreta por PR
|
|
4
4
|
|
|
5
5
|
## 1. Resumen
|
|
6
6
|
|
|
@@ -127,7 +127,7 @@ Cadena transversal que corre antes del dispatch al controller (tracing, auth, lo
|
|
|
127
127
|
- `lib/bug_bunny/consumer_middleware.rb` — `BugBunny::ConsumerMiddleware`
|
|
128
128
|
|
|
129
129
|
## Observability
|
|
130
|
-
Mixin de logging estructurado `key=value` que implementa OTel semantic conventions for messaging; `safe_log` nunca lanza;
|
|
130
|
+
Mixin de logging estructurado `key=value` que implementa OTel semantic conventions for messaging; `safe_log` nunca lanza; redacta a `[FILTERED]` en dos capas — por **NOMBRE de clave** (`sensitive_key?`, substring sobre `SENSITIVE_KEYS`) y por **CONTENIDO del valor** (`redact_value`, para la credencial embebida en texto libre que la capa de clave no ve; `redact_structure` para un `Hash`, redactado antes de serializar para no romper el JSON).
|
|
131
131
|
|
|
132
132
|
**Binding:**
|
|
133
133
|
- `lib/bug_bunny/observability.rb` — `BugBunny::Observability`
|
data/docs/release/release.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> meta: artefacto release · RFC-014 (`accepted`) · generado por `arch-structure`
|
|
4
4
|
> + `arch-enrich` (híbrido RFC-014 §2; nació como piloto manual #51, re-anclado
|
|
5
|
-
> a la RFC vigente) · anclado a
|
|
6
|
-
> `lib/bug_bunny/version.rb`,
|
|
5
|
+
> a la RFC vigente) · anclado a `5697541`
|
|
6
|
+
> (`.github/workflows/release.yml`, `*.gemspec`, `lib/bug_bunny/version.rb`,
|
|
7
|
+
> `CHANGELOG.md`) · fecha 2026-07-31 · cobertura:
|
|
7
8
|
> completa (régimen gema: build→publish, §e/f/g n/a).
|
|
8
9
|
|
|
9
10
|
## 1. Resumen
|
|
@@ -20,9 +21,9 @@ out-of-repo).
|
|
|
20
21
|
|
|
21
22
|
### a. Hecho verificable
|
|
22
23
|
|
|
23
|
-
- **Convención de versión:** SemVer `vX.X.X`. Actual: **
|
|
24
|
-
- **Source of truth:** tag remoto (`
|
|
25
|
-
`lib/bug_bunny/version.rb` (`VERSION = '
|
|
24
|
+
- **Convención de versión:** SemVer `vX.X.X`. Actual: **5.1.1**.
|
|
25
|
+
- **Source of truth:** tag remoto (`v5.1.1`) + **triple mirror**
|
|
26
|
+
`lib/bug_bunny/version.rb` (`VERSION = '5.1.1'`) ← `bug_bunny.gemspec:7`
|
|
26
27
|
(`spec.version = BugBunny::VERSION`).
|
|
27
28
|
- **Changelog canónico:** `CHANGELOG.md` único.
|
|
28
29
|
- **Patrón de trigger:** `gema-tag` (patrón 1).
|
|
@@ -33,7 +34,7 @@ out-of-repo).
|
|
|
33
34
|
|
|
34
35
|
- **Convención:** SemVer `vX.X.X` (**con `v`** — distinto al servicio).
|
|
35
36
|
- **Source of truth:** tag remoto canónico (`git tag --sort=-v:refname` →
|
|
36
|
-
`
|
|
37
|
+
`v5.1.1`).
|
|
37
38
|
- **Mirror:** `lib/bug_bunny/version.rb` (`VERSION`), leído por
|
|
38
39
|
`bug_bunny.gemspec:7` (`spec.version = BugBunny::VERSION`).
|
|
39
40
|
`required_ruby_version >= 2.6.0` (`bug_bunny.gemspec:17`).
|
|
@@ -58,8 +59,8 @@ out-of-repo).
|
|
|
58
59
|
`on: push: tags: ['v*']` → `ruby/setup-ruby@v1` → `gem build *.gemspec` +
|
|
59
60
|
`gem push *.gem` (auth `secrets.RUBYGEMS_API_KEY`). Auditable y versionado con
|
|
60
61
|
el código; se ancla a `file:line`, no se referencia como caja negra.
|
|
61
|
-
- **Consumo:** los servicios la pinnean por versión (`gem "bug_bunny", "~>
|
|
62
|
-
desde RubyGems — **no** git-source.
|
|
62
|
+
- **Consumo:** los servicios la pinnean por versión (`gem "bug_bunny", "~> X.Y.0"`,
|
|
63
|
+
hoy `~> 5.1.0`) desde RubyGems — **no** git-source.
|
|
63
64
|
|
|
64
65
|
### e. Deploy / publish
|
|
65
66
|
|
|
@@ -79,12 +80,19 @@ procedimiento per-repo porque no vive acá. Una versión yankeada se anotaría e
|
|
|
79
80
|
|
|
80
81
|
### h. Dependencias de deploy inter-servicio
|
|
81
82
|
|
|
82
|
-
- **Consumidores** (cruza RFC-018): servicios del fleet la pinnean
|
|
83
|
-
`~>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
- **Consumidores** (cruza RFC-018): servicios del fleet la pinnean con la forma
|
|
84
|
+
`~> X.Y.0` — **patch-compatible**, no minor: `~> 5.1.0` resuelve
|
|
85
|
+
`>= 5.1.0, < 5.2.0`, así que un minor **no entra** sin editar el `Gemfile`.
|
|
86
|
+
Consecuencia operativa: un fix publicado como **patch** lo toman con
|
|
87
|
+
`bundle update bug_bunny`; uno publicado como **minor** requiere tocar el pin
|
|
88
|
+
en cada consumidor. Estado hoy: `box_manager_service` y `box_radius_manager`
|
|
89
|
+
en `~> 5.1.0`; `box_acs_manager` y `box_cluster_manager` todavía en
|
|
90
|
+
`~> 4.19.0` (bumps pendientes). Un cambio de contrato del gem (ej. el
|
|
91
|
+
behavior-change de `4.18.0` — `Bunny::Exception` → `CommunicationError`, o la
|
|
92
|
+
eliminación de `BugBunny::SecurityError` en `5.0.0`) obliga a los consumidores
|
|
93
|
+
a migrar; el `CHANGELOG.md` lo documenta como breaking note. **Orden de
|
|
94
|
+
deploy:** los consumidores adoptan al hacer `bundle update bug_bunny` — no hay
|
|
95
|
+
deploy coordinado (cada servicio elige cuándo).
|
|
88
96
|
|
|
89
97
|
### i. Contrato con la skill productora
|
|
90
98
|
|
data/lib/bug_bunny/exception.rb
CHANGED
|
@@ -23,11 +23,15 @@ module BugBunny
|
|
|
23
23
|
# respuesta RPC (ej: {CommunicationError}, {ConfigurationError}).
|
|
24
24
|
#
|
|
25
25
|
# @note **No loguear ni enviar a sinks (Sentry/logs) sin sanitizar.** El
|
|
26
|
-
# cuerpo crudo puede contener datos sensibles (p. ej. en `details`).
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
26
|
+
# cuerpo crudo puede contener datos sensibles (p. ej. en `details`). La
|
|
27
|
+
# lista canónica de claves sensibles es
|
|
28
|
+
# {BugBunny::Observability::SENSITIVE_KEYS} — no la repliques acá ni en el
|
|
29
|
+
# consumidor: se desincroniza (ojo que `pass` bare NO está en la lista, a
|
|
30
|
+
# propósito, para no filtrar `passport_number`). Para sanear, reusá
|
|
31
|
+
# {BugBunny::Observability.sensitive_key?} (filtra por NOMBRE de clave) y
|
|
32
|
+
# {BugBunny::Observability.redact_structure} (recorre la estructura y
|
|
33
|
+
# además redacta credenciales embebidas en el VALOR). La gema entrega el
|
|
34
|
+
# cuerpo crudo a propósito; sanitizarlo es responsabilidad del consumidor.
|
|
31
35
|
attr_accessor :raw_response
|
|
32
36
|
|
|
33
37
|
# @return [Integer, nil] El código de estado de la respuesta que originó el
|
|
@@ -27,6 +27,81 @@ module BugBunny
|
|
|
27
27
|
SENSITIVE_KEYS.any? { |sensitive| key_str.include?(sensitive) }
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Alternación de keys sensibles ordenada de más larga a más corta: en un regex
|
|
31
|
+
# la alternación matchea leftmost-first, así que sin este orden `auth` ganaría
|
|
32
|
+
# sobre `authorization` y el patrón dejaría de matchear (`orization=x` no sigue
|
|
33
|
+
# con `[:=]`).
|
|
34
|
+
SENSITIVE_KEYS_ALTERNATION = SENSITIVE_KEYS.sort_by { |k| -k.length }.join('|').freeze
|
|
35
|
+
|
|
36
|
+
# Reglas de VALOR sensible, como pares `[regex, reemplazo]`.
|
|
37
|
+
#
|
|
38
|
+
# {.sensitive_key?} solo ve el NOMBRE de la clave; no puede ver una credencial
|
|
39
|
+
# embebida en TEXTO LIBRE. El caso canónico es el `message` de una excepción
|
|
40
|
+
# inesperada (llega como `reason=` o `error_message=`, nombres no sensibles):
|
|
41
|
+
# un `NoMethodError` sobre un objeto de respuesta HTTP puede arrastrar
|
|
42
|
+
# `Authorization: "Bearer eyJ..."` en su mensaje y el filtro por-clave lo deja
|
|
43
|
+
# pasar entero al log.
|
|
44
|
+
#
|
|
45
|
+
# El reemplazo conserva el nombre de la clave cuando viaja dentro del texto
|
|
46
|
+
# (`token=[FILTERED]`, no `[FILTERED]`): saber QUÉ credencial apareció es
|
|
47
|
+
# diagnóstico útil; su valor no.
|
|
48
|
+
SENSITIVE_VALUE_RULES = [
|
|
49
|
+
# Esquemas de autenticación HTTP: "Bearer <jwt>", "Basic <base64>".
|
|
50
|
+
[/\b(?:bearer|basic)\s+[A-Za-z0-9\-._~+\/]{8,}={0,2}/i, '[FILTERED]'],
|
|
51
|
+
# La key viaja DENTRO del texto: `token=abc`, `password: 'x'`, `"api_key" => "y"`.
|
|
52
|
+
#
|
|
53
|
+
# El prefijo `\w*` va en lugar de un `\b`: `_` es word-char, así que un borde de
|
|
54
|
+
# palabra NO existe dentro de `access_token` ni de `accessToken` y esas variantes
|
|
55
|
+
# se colarían en claro — justo las que {.sensitive_key?} cubre a propósito con
|
|
56
|
+
# substring matching. Se captura el prefijo para conservar el nombre COMPLETO de la
|
|
57
|
+
# key en el log (`access_token=[FILTERED]`): saber qué credencial apareció es
|
|
58
|
+
# diagnóstico útil. No reintroduce el falso positivo de `passport_number` porque
|
|
59
|
+
# ninguna key de SENSITIVE_KEYS es substring suyo (por eso `pass` bare está excluida).
|
|
60
|
+
[/(\w*(?:#{SENSITIVE_KEYS_ALTERNATION}))["']?\s*(?:=>|[:=])\s*["']?[^\s,;"'}\])]+/i,
|
|
61
|
+
'\1=[FILTERED]'],
|
|
62
|
+
# Credenciales en una URL: `amqp://user:pass@host` → conserva el esquema y el host.
|
|
63
|
+
[%r{(://)[^\s/:@]+:[^\s/@]+@}, '\1[FILTERED]@']
|
|
64
|
+
].freeze
|
|
65
|
+
|
|
66
|
+
# Redacta credenciales embebidas en un valor de texto libre.
|
|
67
|
+
#
|
|
68
|
+
# Complementa a {.sensitive_key?}: esa filtra por NOMBRE de clave, esta por
|
|
69
|
+
# CONTENIDO. Se aplica a todo valor no numérico que {#safe_log} serializa.
|
|
70
|
+
#
|
|
71
|
+
# @param value [Object] El valor a redactar (se serializa con `to_s`).
|
|
72
|
+
# @return [String] El valor con las credenciales reemplazadas por `[FILTERED]`.
|
|
73
|
+
def self.redact_value(value)
|
|
74
|
+
SENSITIVE_VALUE_RULES.reduce(value.to_s) do |acc, (pattern, replacement)|
|
|
75
|
+
acc.gsub(pattern, replacement)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Redacta una estructura ANTES de serializarla, recorriendo keys y valores.
|
|
80
|
+
#
|
|
81
|
+
# Se usa para los valores `Hash` de {#safe_log}. Redactar el JSON ya serializado con
|
|
82
|
+
# {.redact_value} no sirve: la regla de key-dentro-del-texto normaliza el separador a
|
|
83
|
+
# `=` y se come la comilla de cierre de la key, dejando un objeto donde el par
|
|
84
|
+
# `"token": "abc"` quedó colapsado en `"token=[FILTERED]"` — el secreto desaparece,
|
|
85
|
+
# pero el campo deja de ser JSON parseable y quien consume el log pierde el objeto
|
|
86
|
+
# entero, no solo el valor redactado.
|
|
87
|
+
#
|
|
88
|
+
# Recorriendo la estructura, además, las keys internas SÍ pasan por {.sensitive_key?}
|
|
89
|
+
# (que solo veía las keys de primer nivel del metadata).
|
|
90
|
+
#
|
|
91
|
+
# @param obj [Object] Estructura a redactar (Hash/Array anidados incluidos).
|
|
92
|
+
# @return [Object] La misma forma, con los valores sensibles reemplazados.
|
|
93
|
+
def self.redact_structure(obj)
|
|
94
|
+
case obj
|
|
95
|
+
when Hash
|
|
96
|
+
obj.each_with_object({}) do |(k, v), acc|
|
|
97
|
+
acc[k] = sensitive_key?(k) ? '[FILTERED]' : redact_structure(v)
|
|
98
|
+
end
|
|
99
|
+
when Array then obj.map { |element| redact_structure(element) }
|
|
100
|
+
when Numeric, TrueClass, FalseClass, NilClass then obj
|
|
101
|
+
else redact_value(obj)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
30
105
|
private
|
|
31
106
|
|
|
32
107
|
# Registra un evento estructurado. Nunca eleva excepciones.
|
|
@@ -43,12 +118,17 @@ module BugBunny
|
|
|
43
118
|
val = BugBunny::Observability.sensitive_key?(k) ? '[FILTERED]' : v
|
|
44
119
|
next if val.nil?
|
|
45
120
|
|
|
121
|
+
# La redacción por CONTENIDO se aplica a todo valor no numérico: el filtro
|
|
122
|
+
# por-clave de arriba no ve una credencial embebida en texto libre.
|
|
46
123
|
formatted = case val
|
|
47
124
|
when Numeric then val
|
|
48
125
|
when Hash
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
126
|
+
# Se redacta la estructura y DESPUÉS se serializa: al revés el campo
|
|
127
|
+
# queda con el secreto tapado pero el JSON roto (ver .redact_structure).
|
|
128
|
+
BugBunny::Observability.redact_structure(val).to_json
|
|
129
|
+
else
|
|
130
|
+
redacted = BugBunny::Observability.redact_value(val)
|
|
131
|
+
redacted.include?(' ') ? redacted.inspect : redacted
|
|
52
132
|
end
|
|
53
133
|
"#{k}=#{formatted}"
|
|
54
134
|
end.compact.join(' ')
|
data/lib/bug_bunny/version.rb
CHANGED
data/skill/SKILL.md
CHANGED
|
@@ -19,7 +19,9 @@ Gema Ruby: capa de routing RESTful sobre AMQP/RabbitMQ. Microservicios se comuni
|
|
|
19
19
|
|
|
20
20
|
## Contrato resumido (piso mínimo)
|
|
21
21
|
|
|
22
|
-
> Resume el contrato de **`bug_bunny`
|
|
22
|
+
> Resume el contrato de **`bug_bunny` 5.1.1** (anclado a `v5.1.1`). Suficiente para el uso típico sin abrir el detalle; el detalle version-locked está en el **Índice de artefactos** de abajo. Antipatrones/API completa: más abajo (embebido interim, ver Cobertura y fronteras).
|
|
23
|
+
>
|
|
24
|
+
> **Si venís de 4.x, dos breaking a mirar antes de subir:** `5.0.0` eliminó la constante pública `BugBunny::SecurityError` (un `rescue BugBunny::SecurityError` que sobreviva revienta con `NameError` y **enmascara la excepción original**) y `4.18.0` cambió el wrapping `Bunny::Exception` → `CommunicationError`. Detalle en `CHANGELOG.md`.
|
|
23
25
|
|
|
24
26
|
**Símbolos públicos clave**
|
|
25
27
|
|
|
@@ -58,14 +60,21 @@ client.publish('events', body: { type: 'x' }) # => { 'status' => 202 }
|
|
|
58
60
|
|
|
59
61
|
## Índice de artefactos (fuente de verdad)
|
|
60
62
|
|
|
61
|
-
El detalle vive en `docs/<capa>/` (modelo `dev-*`); esta skill **indexa y resume**, no duplica. Links relativos = version-locked (mismo tag del release
|
|
63
|
+
El detalle vive en `docs/<capa>/` (modelo `dev-*`); esta skill **indexa y resume**, no duplica. Links relativos = version-locked (mismo tag del release, `v5.1.1`; `gemspec.files` incluye `docs/**`, así que estos archivos viajan dentro del `.gem` que ya tenés instalado).
|
|
62
64
|
|
|
63
65
|
| Capa | Artefacto | Estado |
|
|
64
66
|
|---|---|---|
|
|
65
67
|
| Glosario de dominio | [docs/glossary/glossary.md](../docs/glossary/glossary.md) | parcial, acreta por PR |
|
|
66
68
|
| Comportamiento (flujos) | [docs/behavior/behavior.md](../docs/behavior/behavior.md) | completa — 6 flujos |
|
|
69
|
+
| Configuración | [docs/config/configuracion.md](../docs/config/configuracion.md) | §a-§e/§i estructura + §f/§g/§h enrich — completa |
|
|
70
|
+
| Dependencias consumidas | [docs/consumed/rabbitmq.md](../docs/consumed/rabbitmq.md) | §a/§b/§d estructura + §c/§e enrich |
|
|
71
|
+
| Errores | [docs/errors/errors.md](../docs/errors/errors.md) | §a/§b/§d completas; §c política inferida (verificación humana pendiente) |
|
|
72
|
+
| Test | [docs/test/testing.md](../docs/test/testing.md) | §a-§h — 16 unit / 7 integration |
|
|
73
|
+
| Release | [docs/release/release.md](../docs/release/release.md) | completa (régimen gema: build→publish; §e/f/g n/a) |
|
|
67
74
|
| Datos | — | n/a — gema sin DB |
|
|
68
|
-
|
|
|
75
|
+
| Eventos | — | n/a — la gema **es** el transporte; no declara catálogo de eventos de dominio propio |
|
|
76
|
+
| Seguridad | — | n/a — sin authn/authz propias; el guard anti-RCE (403) está en `docs/errors/` |
|
|
77
|
+
| Operaciones / Interfaz / Topología | — | pendiente — ver Cobertura y fronteras |
|
|
69
78
|
|
|
70
79
|
> **Glosario:** migrado a [docs/glossary/glossary.md](../docs/glossary/glossary.md)
|
|
71
80
|
> (RFC-008 §2 — el compuesto referencia, no copia). Términos AMQP base
|
|
@@ -74,13 +83,15 @@ El detalle vive en `docs/<capa>/` (modelo `dev-*`); esta skill **indexa y resume
|
|
|
74
83
|
|
|
75
84
|
## Cobertura y fronteras
|
|
76
85
|
|
|
77
|
-
**Coexistencia transitoria con destino pendiente (RFC-008 §2 — interim de migración):** mientras
|
|
86
|
+
**Coexistencia transitoria con destino pendiente (RFC-008 §2 — interim de migración):** mientras las capas `docs/api/` (operaciones), `docs/interface/` y `docs/topology/` no estén generadas para este repo, el contrato que les correspondería permanece embebido bajo el interim normado:
|
|
78
87
|
|
|
79
88
|
- **En esta skill (abajo):** el contrato detallado (jerarquía de excepciones, API de config, modos de entrega) **y** el diagrama de arquitectura (flujo RPC). El *Contrato resumido* de arriba es el piso mínimo (RFC-008 §2); lo de abajo es el detalle interim hasta que exista `docs/api|interface|topology`.
|
|
80
89
|
- **En `README.md`:** el contrato (sin el diagrama de arquitectura).
|
|
81
90
|
- **Guías how-to** (`references/*.md`, pre-estándar): destino futuro `docs/howto/`.
|
|
82
91
|
|
|
83
|
-
Por RFC-008 §2: no se fabrica la capa, no se borra contrato sin destino, no se duplica; migra cuando
|
|
92
|
+
Por RFC-008 §2: no se fabrica la capa, no se borra contrato sin destino, no se duplica; migra cuando se generen, mismo PR. Estado transitorio declarado, no excepción permanente. Origen del gap (resuelto, normado): [sequre/ai_knowledge#95](https://github.com/sequre/ai_knowledge/issues/95).
|
|
93
|
+
|
|
94
|
+
> **Nota de alcance (2026-07-31):** la justificación original de este interim era que el generador no implementaba esas capas. Ya las implementa, así que el pendiente es de **este repo**, no de tooling — en particular `docs/interface/` (RFC-004), donde correspondería registrar la API pública (incluidos `Observability.redact_value` y `.redact_structure`, agregados en `5.1.1`). Queda como trabajo propio, fuera del alcance de este release.
|
|
84
95
|
|
|
85
96
|
---
|
|
86
97
|
|
|
@@ -129,7 +140,7 @@ Por RFC-008 §2: no se fabrica la capa, no se borra contrato sin destino, no se
|
|
|
129
140
|
| `BugBunny::Controller` | Base class tipo Rails. `before_action`, `around_action`, `after_action`, `rescue_from`, `render`. |
|
|
130
141
|
| `BugBunny::Resource` | ORM sobre AMQP. `find`, `where`, `create`, `save`, `destroy`. ActiveModel validations y callbacks. |
|
|
131
142
|
| `BugBunny::Routing::RouteSet` | DSL de rutas: `resources`, `namespace`, `member`, `collection`. |
|
|
132
|
-
| `BugBunny::Observability` | Mixin de logging estructurado. `safe_log` nunca lanza excepciones.
|
|
143
|
+
| `BugBunny::Observability` | Mixin de logging estructurado. `safe_log` nunca lanza excepciones. Redacta credenciales en dos capas: por nombre de clave (`sensitive_key?`) y por contenido del valor (`redact_value` / `redact_structure`, para la credencial embebida en texto libre). |
|
|
133
144
|
| `BugBunny::Middleware::Stack` | Builder de middlewares client-side (onion architecture tipo Faraday). |
|
|
134
145
|
| BugBunny::Request | Value object del mensaje saliente con metadata AMQP completa. |
|
|
135
146
|
| BugBunny::OTel | Helpers para emitir campos siguiendo las OTel semantic conventions for messaging. |
|
data/skill/references/errores.md
CHANGED
|
@@ -37,9 +37,13 @@ rescue BugBunny::Error => e
|
|
|
37
37
|
end
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
> ⚠️ **No loguear `raw_response` crudo.** Puede contener datos sensibles.
|
|
41
|
-
>
|
|
42
|
-
>
|
|
40
|
+
> ⚠️ **No loguear `raw_response` crudo.** Puede contener datos sensibles. Sanear
|
|
41
|
+
> antes de Sentry/logs con `Observability.redact_structure` (recorre la
|
|
42
|
+
> estructura: filtra por nombre de clave y también redacta la credencial
|
|
43
|
+
> embebida en el valor). **No replicar la lista de claves** — la canónica es
|
|
44
|
+
> `Observability::SENSITIVE_KEYS`; replicarla la desincroniza (y `pass` bare NO
|
|
45
|
+
> está en ella, a propósito, para no filtrar `passport_number`). La gema entrega
|
|
46
|
+
> el cuerpo crudo a propósito; sanitizar es del consumidor.
|
|
43
47
|
|
|
44
48
|
Para consumir un envelope estructurado de dominio (ej. `{ error: { code,
|
|
45
49
|
message, details } }`), parsealo en el boundary del servicio desde
|
|
@@ -29,6 +29,164 @@ RSpec.describe BugBunny::Observability do
|
|
|
29
29
|
log_output.string.split("\n").last.to_s.sub(/\A.*?:\s*/, '')
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
describe '.redact_value (contenido sensible en texto libre)' do
|
|
33
|
+
# Complementa a .sensitive_key?: esa mira el NOMBRE de la clave, esta el
|
|
34
|
+
# CONTENIDO. El caso que motiva la feature es el `message` de una excepción.
|
|
35
|
+
it 'redacta un Bearer token' do
|
|
36
|
+
redacted = BugBunny::Observability.redact_value(
|
|
37
|
+
'undefined method for #<Faraday::Response headers={"Authorization"=>"Bearer eyJhbGciOiJIUzI1NiJ9.abc"}>'
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
expect(redacted).not_to include('eyJhbGciOiJIUzI1NiJ9.abc')
|
|
41
|
+
expect(redacted).to include('[FILTERED]')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'redacta un esquema Basic' do
|
|
45
|
+
redacted = BugBunny::Observability.redact_value('Authorization: Basic dXNlcjpwYXNzd29yZA==')
|
|
46
|
+
|
|
47
|
+
expect(redacted).not_to include('dXNlcjpwYXNzd29yZA')
|
|
48
|
+
expect(redacted).to include('[FILTERED]')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'redacta el valor cuando la key viaja DENTRO del texto, conservando el nombre' do
|
|
52
|
+
redacted = BugBunny::Observability.redact_value('connect failed (token=abc123, host=rabbit)')
|
|
53
|
+
|
|
54
|
+
expect(redacted).not_to include('abc123')
|
|
55
|
+
expect(redacted).to include('token=[FILTERED]')
|
|
56
|
+
# El resto del mensaje sobrevive: la redacción es quirúrgica, no destructiva.
|
|
57
|
+
expect(redacted).to include('host=rabbit')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'matchea la key más larga primero (authorization no se parte en auth)' do
|
|
61
|
+
redacted = BugBunny::Observability.redact_value('authorization: "Bearer-less-secret-value"')
|
|
62
|
+
|
|
63
|
+
expect(redacted).not_to include('Bearer-less-secret-value')
|
|
64
|
+
expect(redacted).to include('authorization=[FILTERED]')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'redacta credenciales de una URL conservando esquema y host' do
|
|
68
|
+
redacted = BugBunny::Observability.redact_value('amqp://guest:s3cr3t@rabbit:5672/vhost')
|
|
69
|
+
|
|
70
|
+
expect(redacted).not_to include('s3cr3t')
|
|
71
|
+
expect(redacted).to include('amqp://[FILTERED]@rabbit:5672/vhost')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'deja intacto un texto sin credenciales' do
|
|
75
|
+
expect(BugBunny::Observability.redact_value('timeout after 30s on queue acs.rpc'))
|
|
76
|
+
.to eq('timeout after 30s on queue acs.rpc')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'no confunde una key no sensible que contiene un substring parecido' do
|
|
80
|
+
expect(BugBunny::Observability.redact_value('passport_number=AB123'))
|
|
81
|
+
.to include('AB123')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# `_` es word-char: un `\b` antes de la key NO encuentra borde dentro de
|
|
85
|
+
# `access_token` ni de `accessToken`, y esas variantes se colaban en claro.
|
|
86
|
+
# Son exactamente las que sensitive_key? cubre a propósito con substring matching.
|
|
87
|
+
it 'redacta la variante con separador (access_token) conservando el nombre completo' do
|
|
88
|
+
redacted = BugBunny::Observability.redact_value('request failed access_token=eyJsecret.jwt')
|
|
89
|
+
|
|
90
|
+
expect(redacted).not_to include('eyJsecret.jwt')
|
|
91
|
+
expect(redacted).to include('access_token=[FILTERED]')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'redacta la variante con prefijo (user_password)' do
|
|
95
|
+
redacted = BugBunny::Observability.redact_value('invalid params user_password=hunter2')
|
|
96
|
+
|
|
97
|
+
expect(redacted).not_to include('hunter2')
|
|
98
|
+
expect(redacted).to include('user_password=[FILTERED]')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'redacta la variante camelCase (accessToken)' do
|
|
102
|
+
redacted = BugBunny::Observability.redact_value('boom accessToken=eyJsecret.jwt')
|
|
103
|
+
|
|
104
|
+
expect(redacted).not_to include('eyJsecret.jwt')
|
|
105
|
+
expect(redacted).to include('accessToken=[FILTERED]')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'no redacta una key no sensible con sufijo numérico (processing_session_count)' do
|
|
109
|
+
expect(BugBunny::Observability.redact_value('processing_session_count=5'))
|
|
110
|
+
.to eq('processing_session_count=5')
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe '.redact_structure (estructura antes de serializar)' do
|
|
115
|
+
it 'redacta por key interna y deja la forma intacta' do
|
|
116
|
+
redacted = BugBunny::Observability.redact_structure(
|
|
117
|
+
'token' => 'abc123', 'host' => 'rabbit'
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
expect(redacted).to eq('token' => '[FILTERED]', 'host' => 'rabbit')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'recorre Hash anidado y Array' do
|
|
124
|
+
redacted = BugBunny::Observability.redact_structure(
|
|
125
|
+
'nested' => { 'api_key' => 'xyz', 'n' => 1 },
|
|
126
|
+
'list' => ['token=abc123', 'clean']
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
expect(redacted).to eq(
|
|
130
|
+
'nested' => { 'api_key' => '[FILTERED]', 'n' => 1 },
|
|
131
|
+
'list' => ['token=[FILTERED]', 'clean']
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'no altera numéricos ni booleanos ni nil' do
|
|
136
|
+
expect(BugBunny::Observability.redact_structure('n' => 1, 'ok' => true, 'x' => nil))
|
|
137
|
+
.to eq('n' => 1, 'ok' => true, 'x' => nil)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe '#safe_log — redacción por contenido' do
|
|
142
|
+
# El caso real: el nombre de la clave NO es sensible (`reason`), así que el
|
|
143
|
+
# filtro por-clave lo deja pasar; la credencial va en el valor.
|
|
144
|
+
it 'filtra una credencial embebida en el valor de una key NO sensible' do
|
|
145
|
+
host.safe_log(:error, 'unhandled_exception',
|
|
146
|
+
reason: 'NoMethodError on headers {"Authorization"=>"Bearer eyJsupersecret.jwt"}')
|
|
147
|
+
|
|
148
|
+
expect(last_log_line).not_to include('eyJsupersecret.jwt')
|
|
149
|
+
expect(last_log_line).to include('[FILTERED]')
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'filtra dentro de un Hash serializado (las keys internas no pasan por sensitive_key?)' do
|
|
153
|
+
host.safe_log(:error, 'unhandled_exception', details: { 'token' => 'abc123xyz' })
|
|
154
|
+
|
|
155
|
+
expect(last_log_line).not_to include('abc123xyz')
|
|
156
|
+
expect(last_log_line).to include('[FILTERED]')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# La redacción no puede costar la estructura: quien consume el log parsea este campo
|
|
160
|
+
# como JSON, y un objeto roto le hace perder TODOS los pares, no solo el redactado.
|
|
161
|
+
it 'mantiene el campo Hash como JSON parseable después de redactar' do
|
|
162
|
+
host.safe_log(:error, 'unhandled_exception',
|
|
163
|
+
details: { 'token' => 'abc123xyz', 'host' => 'rabbit',
|
|
164
|
+
'nested' => { 'api_key' => 'xyz789', 'n' => 1 } })
|
|
165
|
+
|
|
166
|
+
field = last_log_line[/details=(\S+)/, 1]
|
|
167
|
+
|
|
168
|
+
expect { JSON.parse(field) }.not_to raise_error
|
|
169
|
+
expect(JSON.parse(field)).to eq(
|
|
170
|
+
'token' => '[FILTERED]', 'host' => 'rabbit',
|
|
171
|
+
'nested' => { 'api_key' => '[FILTERED]', 'n' => 1 }
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'no altera un valor numérico' do
|
|
176
|
+
host.safe_log(:info, 'done', duration_s: 1.5, status: 200)
|
|
177
|
+
|
|
178
|
+
expect(last_log_line).to include('duration_s=1.5', 'status=200')
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'preserva el resto de la línea (component, event y campos limpios)' do
|
|
182
|
+
host.safe_log(:error, 'request_error', kind: 'unavailable', reason: 'ACS down')
|
|
183
|
+
|
|
184
|
+
line = last_log_line
|
|
185
|
+
expect(line).to include('event=request_error', 'kind=unavailable')
|
|
186
|
+
expect(line).to include('reason="ACS down"')
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
32
190
|
describe '.sensitive_key? (módulo público)' do
|
|
33
191
|
subject(:sensitive?) { BugBunny::Observability.method(:sensitive_key?) }
|
|
34
192
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bug_bunny
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.1.
|
|
4
|
+
version: 5.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -314,7 +314,7 @@ metadata:
|
|
|
314
314
|
homepage_uri: https://github.com/gedera/bug_bunny
|
|
315
315
|
source_code_uri: https://github.com/gedera/bug_bunny
|
|
316
316
|
changelog_uri: https://github.com/gedera/bug_bunny/blob/main/CHANGELOG.md
|
|
317
|
-
documentation_uri: https://github.com/gedera/bug_bunny/blob/v5.1.
|
|
317
|
+
documentation_uri: https://github.com/gedera/bug_bunny/blob/v5.1.1/skill
|
|
318
318
|
post_install_message:
|
|
319
319
|
rdoc_options: []
|
|
320
320
|
require_paths:
|