foam-otel 1.3.0 → 1.4.0
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/GOTCHAS.md +53 -0
- data/README.md +98 -10
- data/THIRD-PARTY-NOTICES +27 -0
- data/lib/foam/otel/config.rb +22 -11
- data/lib/foam/otel/constants.rb +210 -0
- data/lib/foam/otel/ingest.rb +29 -12
- data/lib/foam/otel/init.rb +34 -3
- data/lib/foam/otel/llm.rb +36 -8
- data/lib/foam/otel/metrics.rb +7 -1
- data/lib/foam/otel/pipelines.rb +16 -3
- data/lib/foam/otel/redaction.rb +564 -85
- data/lib/foam/otel/resource.rb +12 -3
- data/lib/foam/otel/version.rb +17 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 180e8af99c1f5c9bfc35de221c4985fb3ebac7b52ddf6fc99d6552a52aaea836
|
|
4
|
+
data.tar.gz: 14a5d81c673af1a20a75e483e076d2c6b1820fa14e5a587e2a23451da3cccf77
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a307d175e903abbbbd1c382e65694664f47aba18408fc7153f9a43e9fb72f8131fedcfc71a59e16ee4f7446dee0f3460233883f96a7048e1284fff75b54e16c7
|
|
7
|
+
data.tar.gz: 7322e0a91e04092d494150ad9b003e4a6fd61b23ebc7468a8c715eec2d5ab8aac64d1edc4c9fb3cb1a844c3d3a56c167b9bbf05bed2bb1d4cb491eba20695f5d
|
data/GOTCHAS.md
CHANGED
|
@@ -582,6 +582,59 @@ anthropic 1.59.0, gemini-ai 4.3.0, ruby_llm 1.16.0).
|
|
|
582
582
|
rack end-to-end with real `traceparent`+`baggage` headers, the no-op and
|
|
583
583
|
never-raise cases.
|
|
584
584
|
|
|
585
|
+
## F13: The value-pattern secret layer + coverage fix + TLS pin (1.4.0, security ruling 2026-07-27)
|
|
586
|
+
|
|
587
|
+
The credential floor masks by NAME and is value-shape-blind; a live secret
|
|
588
|
+
under an innocuous key (`note`, `detail`) sailed past it. Foam telemetry is
|
|
589
|
+
read downstream by LLMs, so ONE leaked credential is prompt-injection
|
|
590
|
+
exfiltratable — the value-pattern secret layer is the required second control
|
|
591
|
+
(`docs/decisions/security-fixes-design.md` CONTRACT V).
|
|
592
|
+
|
|
593
|
+
- **What / where**: `Redaction.scan_value_secrets` masks credential-SHAPED
|
|
594
|
+
spans (`SECRET_VALUE_RULES` + the PEM/PuTTY linear scans + the H1/H2
|
|
595
|
+
heuristics) to `[REDACTED]` in EVERY leaf string the engine walks —
|
|
596
|
+
attribute values, `status.message`, exception events, log bodies (string +
|
|
597
|
+
structured leaves), SQL, LLM content (BEFORE serialization, F-RB2), URL
|
|
598
|
+
query AND fragment pair values (Contract C1), resource identity strings
|
|
599
|
+
(once at build), exemplar `filtered_attributes`. It rides the SAME exporter-
|
|
600
|
+
boundary seam as the floor — one pass, ordered floor → C1 tokenizer → value
|
|
601
|
+
scan → user keys, each hit terminal (`redaction.rb` `mask_one`/
|
|
602
|
+
`redact_leaf_string`). METRIC DATAPOINT ATTRIBUTES are the one mandated
|
|
603
|
+
exemption (`metrics.rb`/`mask_metric_data` pass `value_scan: false`) —
|
|
604
|
+
low-cardinality by construction; a value mask there would break aggregation
|
|
605
|
+
dimensions.
|
|
606
|
+
- **Non-disableable Tier 1**: the rules are frozen module constants, not
|
|
607
|
+
Config — no config path can narrow them (same posture as the floor). ONLY
|
|
608
|
+
the H1/H2 heuristic tier honors `secret_heuristics: false`, logged loud at
|
|
609
|
+
init.
|
|
610
|
+
- **ReDoS-bounded / fail-closed**: compile-once, a lowercase-literal anchor
|
|
611
|
+
pre-filter (no anchor → zero regexes), a 256 KiB value cap (oversize +
|
|
612
|
+
anchor ⇒ whole-value mask, never regex-scanned), a match-flood cap, a
|
|
613
|
+
256-pair query/fragment cap, a per-Regexp `timeout:` on Ruby ≥ 3.2. ANY
|
|
614
|
+
scanner fault (timeout included) masks the value, never a raw pass-through.
|
|
615
|
+
- **F-RB1 TLS pin (CWE-295)**: the vendored OTLP exporter honored
|
|
616
|
+
`OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE` as a PRESENCE check — one env var
|
|
617
|
+
would silently strip cert verification off the token-bearing export
|
|
618
|
+
connection. Every exporter constructor (door 1 `pipelines.rb`, door 2
|
|
619
|
+
`ingest.rb`) now pins `ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER`; the var
|
|
620
|
+
is INERT and warned about (`init.rb` `warn_inert_ssl_verify_none`). There is
|
|
621
|
+
no foam option to weaken it.
|
|
622
|
+
- **Mitigation**: `lib/foam/otel/constants.rb` (the frozen ruleset),
|
|
623
|
+
`lib/foam/otel/redaction.rb` (the engine + coverage tokenizer),
|
|
624
|
+
`lib/foam/otel/pipelines.rb` + `lib/foam/otel/ingest.rb` (the TLS pin),
|
|
625
|
+
`lib/foam/otel/llm.rb` (C4 pre-serialization masking).
|
|
626
|
+
- **Sources**: `docs/decisions/security-fixes-design.md`;
|
|
627
|
+
gitleaks/detect-secrets/secretlint provenance in `THIRD-PARTY-NOTICES`;
|
|
628
|
+
vendored exporter presence-check at
|
|
629
|
+
opentelemetry-exporter-otlp `exporter.rb:39-47`.
|
|
630
|
+
- **Test**: `spec/value_secret_layer_spec.rb` (the per-rule corpus, caps,
|
|
631
|
+
precedence, coverage contract, off-surface pin, structural gates),
|
|
632
|
+
`spec/security_regressions_spec.rb` (F-RB1 + F-RB2 verbatim repros),
|
|
633
|
+
`spec/vuln_suite_spec.rb` (the adversarial exfil/ReDoS/bypass suite +
|
|
634
|
+
mutation spot-checks).
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
585
638
|
## General gotchas (applicable to Ruby)
|
|
586
639
|
|
|
587
640
|
- **G1 — init after target import / pre-init no-op**: the API's proxy providers
|
data/README.md
CHANGED
|
@@ -4,12 +4,15 @@ Foam's OpenTelemetry core for Ruby services. A thin, safe wrapper over the
|
|
|
4
4
|
official OpenTelemetry libraries: foam owns the pipeline (providers, batch
|
|
5
5
|
processors, OTLP export to the foam fleet endpoint), turns on automatic
|
|
6
6
|
tier-1/2 instrumentation, and hands you a small set of never-throw helpers —
|
|
7
|
-
and nothing else. **Redaction is fully opt-in above
|
|
8
|
-
by default foam captures every value RAW** (no
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
and nothing else. **Redaction is fully opt-in above two always-on exceptions:
|
|
8
|
+
by default foam captures every value RAW** (no PII preset) **except (1) the
|
|
9
|
+
credential floor** — a fixed list of credential/secret header and field NAMES
|
|
10
|
+
always masked to `[REDACTED]` (see "The default credential denylist" below) —
|
|
11
|
+
**and (2) the value-pattern secret layer** — credential-SHAPED value spans
|
|
12
|
+
(cloud keys, tokens, JWTs, private-key blocks, `user:pass@` URLs…) masked to
|
|
13
|
+
`[REDACTED]` regardless of field name (see "The value-pattern secret layer"
|
|
14
|
+
below); you enumerate any further fields to mask via
|
|
15
|
+
`redact_keys`/`redact_pii_keys`.
|
|
13
16
|
|
|
14
17
|
Built to `docs/BASE_PACKAGE_SPEC.md`. This README is the manual you integrate
|
|
15
18
|
from.
|
|
@@ -147,6 +150,7 @@ API):** the allowlist must include BOTH headers —
|
|
|
147
150
|
| `additional_instrumentations:` | Array | no | `[]` | Constructed tier-2 instrumentation instances to register (fault-isolated: one that throws is skipped with a `[foam]` warning). |
|
|
148
151
|
| `ignored_outbound_hosts:` | Array<String> | no | `[]` | Hosts whose outbound calls produce no spans — EXTENDS the built-in export-loop guard. For a co-resident agent's intake or a tenant exporter's endpoint. Applies to the clients whose official instrumentation supports host suppression: **Net::HTTP and Excon**. Faraday/HTTP (httprb)/HTTPX have no upstream `untraced_hosts` option — foam warns loudly at init when one of those is bundled (GOTCHAS G13); foam's own export loop is guarded for every client regardless. |
|
|
149
152
|
| `diagnostics:` | Boolean | no | false | Verbose `[foam]` self-reporting of init/health. Warnings and errors are always loud regardless. |
|
|
153
|
+
| `secret_heuristics:` | Boolean | no | true | The value-pattern secret layer's HEURISTIC tier (generic keyword+entropy detection, see "The value-pattern secret layer"). `false` disables ONLY the heuristics — the named provider patterns, the credential floor and the redaction-coverage contract have no off switch. Disabling logs one loud `[foam]` line at init (an explicit, audited decision for telemetry whose legitimate values collide with the heuristics). |
|
|
150
154
|
|
|
151
155
|
Examples for the remaining options (each is runnable as-is):
|
|
152
156
|
|
|
@@ -157,6 +161,16 @@ Foam::Otel.init(name: "checkout-api", environment: env, enabled: true, token: EN
|
|
|
157
161
|
redact_pii_keys: %w[customer_email full_name]) # fully [REDACTED]
|
|
158
162
|
```
|
|
159
163
|
|
|
164
|
+
```ruby
|
|
165
|
+
# Only if your legitimate telemetry collides with the generic secret
|
|
166
|
+
# heuristics (e.g. base64 content-addressed ids masked as high-entropy
|
|
167
|
+
# tokens): disable the HEURISTIC tier only. The named provider patterns and
|
|
168
|
+
# the credential floor stay on — they have no off switch. Logs one loud
|
|
169
|
+
# [foam] line.
|
|
170
|
+
Foam::Otel.init(name: "checkout-api", environment: env, enabled: true, token: ENV.fetch("FOAM_OTEL_TOKEN"),
|
|
171
|
+
secret_heuristics: false)
|
|
172
|
+
```
|
|
173
|
+
|
|
160
174
|
```ruby
|
|
161
175
|
# Constructed tier-2 instrumentation instances (fault-isolated; one that
|
|
162
176
|
# throws is skipped with a [foam] warning, never a crashed boot):
|
|
@@ -242,10 +256,11 @@ floor name in `redact_keys` does NOT downgrade it to a tail mask — the floor
|
|
|
242
256
|
is terminal. `enabled: false` stays fully inert exactly as before (nothing
|
|
243
257
|
exports at all — the floor masks data that leaves; inert mode has none).
|
|
244
258
|
|
|
245
|
-
**Everything else stays RAW
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
259
|
+
**Everything else stays RAW — above the floor and the value layer.** The
|
|
260
|
+
floor matches NAMES only, never values. Credential-SHAPED value spans are the
|
|
261
|
+
value-pattern secret layer's job (next section); UNSHAPED values — bodies,
|
|
262
|
+
query values, SQL text, LLM content, and PII under unlisted names — export
|
|
263
|
+
byte-identical raw, exactly as documented everywhere else in this README.
|
|
249
264
|
|
|
250
265
|
**Why.** Coverage-over-masking remains the mission, but raw `Authorization`
|
|
251
266
|
headers, cookies, and API keys on the wire are a breach in waiting for every
|
|
@@ -278,6 +293,78 @@ concept in every core — the floor itself has NO init surface anywhere):
|
|
|
278
293
|
|
|
279
294
|
---
|
|
280
295
|
|
|
296
|
+
## The value-pattern secret layer (always-on)
|
|
297
|
+
|
|
298
|
+
The credential floor masks by NAME and is deliberately value-shape-blind — a
|
|
299
|
+
live AWS key under the field name `note` would sail past it. Foam telemetry is
|
|
300
|
+
read downstream by LLMs, so a leaked credential is exfiltratable by prompt
|
|
301
|
+
injection; "no leakage at all" is the bar. The value-pattern secret layer is
|
|
302
|
+
the second always-on control (fleet ruling 2026-07-27,
|
|
303
|
+
`docs/decisions/security-fixes-design.md`):
|
|
304
|
+
|
|
305
|
+
**What it masks.** Credential-SHAPED value spans, regardless of the field
|
|
306
|
+
name they hide under: AWS access-key ids and keyword-anchored AWS secrets,
|
|
307
|
+
GCP API keys, Azure client/storage secrets, GitHub tokens and fine-grained
|
|
308
|
+
PATs, GitLab tokens, Slack tokens + webhooks, Stripe secret keys
|
|
309
|
+
(publishable `pk_` keys are deliberately NOT masked), JWTs, PEM private-key
|
|
310
|
+
blocks (`-----BEGIN … PRIVATE KEY` — a truncated block masks to the end of
|
|
311
|
+
the value, fail-closed; `PUBLIC KEY`/`CERTIFICATE` blocks never match) and
|
|
312
|
+
PuTTY PPK keys, `scheme://user:pass@` URI credentials (the whole userinfo,
|
|
313
|
+
username included), `Bearer`/`Basic` auth values, and Anthropic/OpenAI API
|
|
314
|
+
keys. The matched span becomes the literal `[REDACTED]` — no tail, no
|
|
315
|
+
prefix, zero reconstructable bytes; surrounding text keeps its diagnostic
|
|
316
|
+
value (`conn to postgres://[REDACTED]@db:5432 refused`).
|
|
317
|
+
|
|
318
|
+
**The heuristic tier** additionally catches the long tail: a
|
|
319
|
+
keyword-anchored generic pass (`password = hunter2SecretXyz99` inside a log
|
|
320
|
+
body masks its value — gitleaks-derived, entropy-gated, with placeholder/
|
|
321
|
+
stopword/key-context false-positive suppression) and a whole-value
|
|
322
|
+
high-entropy base64 token pass (detect-secrets-derived; pure hex and UUIDs
|
|
323
|
+
are deliberately exempt — trace ids and digests are telemetry's ambient
|
|
324
|
+
vocabulary). Only this tier is disableable: `secret_heuristics: false`
|
|
325
|
+
(one loud `[foam]` line; the named patterns above are not configurable).
|
|
326
|
+
|
|
327
|
+
**Where it runs.** Every value-bearing string on every export path, both
|
|
328
|
+
doors: span/event/link attribute values, `status.message`, exception
|
|
329
|
+
messages, log bodies (string and structured leaves), SQL (`db.statement`),
|
|
330
|
+
LLM content (BEFORE serialization and again at the boundary), URL query AND
|
|
331
|
+
fragment pair values, baggage-derived attributes, exemplar
|
|
332
|
+
`filtered_attributes`, and resource identity strings (once, at init).
|
|
333
|
+
Metric datapoint attributes (metric labels) are the one exempt surface —
|
|
334
|
+
low-cardinality by construction; a value mask there would break your
|
|
335
|
+
aggregation dimensions.
|
|
336
|
+
|
|
337
|
+
**Bounded by design.** Compile-once rules, a literal-anchor pre-filter (no
|
|
338
|
+
anchor in the value → zero regexes run), a 256 KiB value cap (an oversized
|
|
339
|
+
value with an anchor hit anywhere masks WHOLE, fail-closed; without one it
|
|
340
|
+
passes untouched), a match-flood cap, a 256-pair query/fragment tokenizer
|
|
341
|
+
cap, and a per-Regexp timeout on Ruby ≥ 3.2. ANY scanner fault masks the
|
|
342
|
+
value rather than shipping it raw.
|
|
343
|
+
|
|
344
|
+
**Provenance.** The ruleset derives from MIT/Apache-licensed OSS secret
|
|
345
|
+
scanners (gitleaks, detect-secrets, secretlint) — see `THIRD-PARTY-NOTICES`.
|
|
346
|
+
Rules are frozen module constants, byte-consistent across the foam cores,
|
|
347
|
+
changed only by fleet ruling.
|
|
348
|
+
|
|
349
|
+
> **Migration note — 1.4.0 (minor): value-pattern secret masking + redaction
|
|
350
|
+
> coverage fix + TLS pin.** foam now masks credential-SHAPED values
|
|
351
|
+
> (AWS/GCP/Azure keys, GitHub/GitLab tokens, Slack, Stripe, Anthropic/OpenAI
|
|
352
|
+
> keys, JWTs, private-key blocks, `user:pass@` URI credentials, bearer/basic
|
|
353
|
+
> tokens) to `[REDACTED]` in every exported string value — regardless of
|
|
354
|
+
> field name — plus a generic keyword+entropy heuristic you can disable with
|
|
355
|
+
> `secret_heuristics: false` (the named patterns and the credential floor
|
|
356
|
+
> cannot be disabled). Redaction now also reaches URL FRAGMENTS (OAuth
|
|
357
|
+
> implicit-flow tokens, hash-router params), bare `k=v` pair lists, nested
|
|
358
|
+
> attribute containers (fail-closed), and structured LLM content BEFORE
|
|
359
|
+
> serialization. Query/fragment tokenizing is capped at 256 pairs (the
|
|
360
|
+
> overflow remainder masks whole). Metric labels are not value-scanned. TLS
|
|
361
|
+
> peer verification is now PINNED on every exporter:
|
|
362
|
+
> `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE` is INERT and loudly warned about.
|
|
363
|
+
> If a dashboard keyed off a raw token value (it should not have), it will
|
|
364
|
+
> now see `[REDACTED]`.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
281
368
|
## The helpers
|
|
282
369
|
|
|
283
370
|
All helpers never raise, and no-op silently before `init` and when disabled.
|
|
@@ -668,6 +755,7 @@ end
|
|
|
668
755
|
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | INERT — foam wires ONE resolved endpoint into all three exporters explicitly, so per-signal endpoint vars never redirect (or split) foam's export. Set → loud `[foam]` warning that it is inert. |
|
|
669
756
|
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | INERT — as above (warns when set). |
|
|
670
757
|
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | INERT — as above (warns when set). |
|
|
758
|
+
| `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE` | INERT — foam pins TLS peer verification (`ssl_verify_mode: VERIFY_PEER`) on every exporter constructor, both doors, so this vendored-exporter knob can never strip certificate verification off the credential-bearing export connection (1.4.0 security fix, CWE-295). Set → loud `[foam]` warning that it is inert; fix the trust chain (CA bundle) instead. |
|
|
671
759
|
| every other `OTEL_*` | INERT — `OTEL_SERVICE_NAME` / `OTEL_RESOURCE_ATTRIBUTES` never override the init-declared identity; sampler/exporter/span-limit vars are ignored. |
|
|
672
760
|
|
|
673
761
|
---
|
data/THIRD-PARTY-NOTICES
CHANGED
|
@@ -20,6 +20,33 @@ filter_parameters (MIT). Mask FORMATS are facts, not copyrightable; no code
|
|
|
20
20
|
was copied. None of these libraries is a runtime dependency of foam-otel.
|
|
21
21
|
Provenance: RESEARCH.md.
|
|
22
22
|
|
|
23
|
+
The VALUE-PATTERN SECRET LAYER ruleset (lib/foam/otel/constants.rb —
|
|
24
|
+
SECRET_VALUE_RULES, SECRET_HEURISTIC_H1/H2, SECRET_VALUE_PLACEHOLDERS,
|
|
25
|
+
SECRET_VALUE_STOPWORDS, SECRET_KEY_CONTEXT_ALLOWLIST; fleet ruling 2026-07-27,
|
|
26
|
+
docs/decisions/security-fixes-design.md CONTRACT V) is likewise COPIED
|
|
27
|
+
KNOWLEDGE derived from permissively-licensed open-source secret scanners, not
|
|
28
|
+
vendored code (rule 38). Regex facts about public token formats are not
|
|
29
|
+
copyrightable; the safe posture is followed anyway — every pattern derives
|
|
30
|
+
from the MIT/Apache source text below or from the vendor's own public token
|
|
31
|
+
format. None of these tools is a runtime dependency of foam-otel.
|
|
32
|
+
|
|
33
|
+
gitleaks — MIT — https://github.com/gitleaks/gitleaks (commit b58d3f1)
|
|
34
|
+
(aws/gcp/azure/github/gitlab/slack/stripe/jwt/openai token
|
|
35
|
+
shapes; the generic-api-key H1 template; the placeholder +
|
|
36
|
+
stopword + key-context allowlists)
|
|
37
|
+
detect-secrets — Apache-2.0 — https://github.com/Yelp/detect-secrets (commit 5e14193)
|
|
38
|
+
(the Base64HighEntropyString H2 entropy tier; several
|
|
39
|
+
provider prefixes and the all-numeric entropy penalty)
|
|
40
|
+
secretlint — MIT — https://github.com/secretlint/secretlint (commit 7da613e)
|
|
41
|
+
(bounded-JS-regex authoring discipline; the DB
|
|
42
|
+
connection-string / URI-userinfo shape; the anthropic key
|
|
43
|
+
shape)
|
|
44
|
+
|
|
45
|
+
trufflehog (AGPL-3.0) was consulted ONLY as a provider-coverage CHECKLIST —
|
|
46
|
+
NO pattern is transcribed from its source. Recorded deliberate: the AGPL
|
|
47
|
+
GCP service-account JSON pattern is NOT adopted; that secret is covered by
|
|
48
|
+
the private-key-block linear scan over its embedded PEM instead.
|
|
49
|
+
|
|
23
50
|
Direct runtime dependencies (name version — license — repository):
|
|
24
51
|
|
|
25
52
|
opentelemetry-api 1.11.0 — Apache-2.0 — https://github.com/open-telemetry/opentelemetry-ruby
|
data/lib/foam/otel/config.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Foam
|
|
|
12
12
|
Config = Struct.new(
|
|
13
13
|
:name, :environment, :version, :enabled,
|
|
14
14
|
:redact_keys, :redact_pii_keys, :ignored_outbound_hosts,
|
|
15
|
-
:diagnostics, :endpoint,
|
|
15
|
+
:diagnostics, :endpoint, :secret_heuristics,
|
|
16
16
|
keyword_init: true
|
|
17
17
|
)
|
|
18
18
|
|
|
@@ -24,7 +24,8 @@ module Foam
|
|
|
24
24
|
name: "", environment: "", version: nil, enabled: false,
|
|
25
25
|
redact_keys: [].freeze, redact_pii_keys: [].freeze,
|
|
26
26
|
ignored_outbound_hosts: [].freeze,
|
|
27
|
-
diagnostics: false, endpoint: FOAM_OTEL_ENDPOINT
|
|
27
|
+
diagnostics: false, endpoint: FOAM_OTEL_ENDPOINT,
|
|
28
|
+
secret_heuristics: true
|
|
28
29
|
).freeze
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -36,7 +37,7 @@ module Foam
|
|
|
36
37
|
|
|
37
38
|
def resolve_config(name:, environment:, version:, enabled:,
|
|
38
39
|
redact_keys:, redact_pii_keys:, ignored_outbound_hosts:,
|
|
39
|
-
diagnostics:, endpoint:)
|
|
40
|
+
diagnostics:, endpoint:, secret_heuristics: true)
|
|
40
41
|
Config.new(
|
|
41
42
|
name: name,
|
|
42
43
|
environment: environment,
|
|
@@ -51,7 +52,14 @@ module Foam
|
|
|
51
52
|
redact_pii_keys: downcase_list(redact_pii_keys),
|
|
52
53
|
ignored_outbound_hosts: Array(ignored_outbound_hosts).map { |h| h.to_s.downcase }.freeze,
|
|
53
54
|
diagnostics: diagnostics ? true : false,
|
|
54
|
-
endpoint: endpoint
|
|
55
|
+
endpoint: endpoint,
|
|
56
|
+
# The value-pattern layer's HEURISTIC tier opt-out (security-fixes-
|
|
57
|
+
# design §V.8): false disables H1/H2 ONLY — the named Tier-1
|
|
58
|
+
# patterns, the credential floor and the coverage contract are
|
|
59
|
+
# module-constant machinery no config shape can narrow. Anything
|
|
60
|
+
# but literal false means ON (default-on preserves the fleet's
|
|
61
|
+
# no-leakage bar; disabling is an explicit, audited decision).
|
|
62
|
+
secret_heuristics: secret_heuristics == false ? false : true
|
|
55
63
|
).freeze
|
|
56
64
|
end
|
|
57
65
|
|
|
@@ -61,12 +69,14 @@ module Foam
|
|
|
61
69
|
Array(list).map { |k| k.to_s.downcase }.reject(&:empty?).uniq.freeze
|
|
62
70
|
end
|
|
63
71
|
|
|
64
|
-
# A second init() refreshes ONLY the redaction lists (and diagnostics
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
|
|
72
|
+
# A second init() refreshes ONLY the redaction lists (and diagnostics,
|
|
73
|
+
# and the heuristic-tier flag — redaction state) onto the EXISTING
|
|
74
|
+
# config: enabled/endpoint/identity are process-global and immutable
|
|
75
|
+
# after the first init (see init.rb — a second init must never flip
|
|
76
|
+
# enabled and leave foam half-dark). Private: not public API surface
|
|
77
|
+
# (pinned by export_surface_spec).
|
|
78
|
+
def refresh_post_init_config(existing, redact_keys:, redact_pii_keys:, diagnostics:,
|
|
79
|
+
secret_heuristics: true)
|
|
70
80
|
Config.new(
|
|
71
81
|
name: existing.name,
|
|
72
82
|
environment: existing.environment,
|
|
@@ -76,7 +86,8 @@ module Foam
|
|
|
76
86
|
redact_pii_keys: downcase_list(redact_pii_keys),
|
|
77
87
|
ignored_outbound_hosts: existing.ignored_outbound_hosts,
|
|
78
88
|
diagnostics: diagnostics ? true : false,
|
|
79
|
-
endpoint: existing.endpoint
|
|
89
|
+
endpoint: existing.endpoint,
|
|
90
|
+
secret_heuristics: secret_heuristics == false ? false : true
|
|
80
91
|
).freeze
|
|
81
92
|
end
|
|
82
93
|
end
|
data/lib/foam/otel/constants.rb
CHANGED
|
@@ -132,5 +132,215 @@ module Foam
|
|
|
132
132
|
REDACTED = "[REDACTED]"
|
|
133
133
|
MASK_BODY = "********" # always exactly eight chars — never length-revealing
|
|
134
134
|
TAIL_MIN_LENGTH = 12 # below this, no tail is safe
|
|
135
|
+
|
|
136
|
+
# ---- THE VALUE-PATTERN SECRET LAYER (fleet ruling 2026-07-27) -----------
|
|
137
|
+
# docs/decisions/security-fixes-design.md CONTRACT V: the always-on second
|
|
138
|
+
# control the credential floor needs — the floor masks by NAME and is
|
|
139
|
+
# value-shape-blind by design, so a credential-SHAPED value under an
|
|
140
|
+
# innocuous key would still export raw (X1 amplifier; foam telemetry is
|
|
141
|
+
# read downstream by LLMs, so a leaked credential is exfiltratable by
|
|
142
|
+
# prompt injection). These constants are the frozen ruleset the central
|
|
143
|
+
# engine (redaction.rb scan_value_secrets) consults DIRECTLY — module-level
|
|
144
|
+
# frozen constants, never Config state, same structural non-disableability
|
|
145
|
+
# as the floor (design §V.8: Tier 1 has NO off switch; only the H1/H2
|
|
146
|
+
# heuristic tier honors the documented secret_heuristics: false opt-out).
|
|
147
|
+
#
|
|
148
|
+
# Licensing/provenance (design §V.1): every pattern derives from MIT
|
|
149
|
+
# (gitleaks b58d3f1, secretlint 7da613e) or Apache-2.0 (detect-secrets
|
|
150
|
+
# 5e14193) sources — see THIRD-PARTY-NOTICES; AGPL trufflehog was used as
|
|
151
|
+
# a coverage CHECKLIST only, no pattern transcribed. FLEET-FIXTURE NOTE:
|
|
152
|
+
# the design mandates a shared, byte-identity-gated
|
|
153
|
+
# contract/secret-patterns.json these constants mirror; that fixture is
|
|
154
|
+
# authored fleet-side (outside ruby/foam-otel's edit lane) — these
|
|
155
|
+
# constants faithfully implement design §V.2–V.4 and are this core's
|
|
156
|
+
# authoritative copy until the shared fixture lands and
|
|
157
|
+
# spec/value_secret_layer_spec.rb's identity gate flips from lint-only to
|
|
158
|
+
# byte-identity.
|
|
159
|
+
|
|
160
|
+
# Execution bounds (design §V.5 / C-caps). All overflow behaviors are
|
|
161
|
+
# FAIL-CLOSED: mask, never raw-pass, never crash.
|
|
162
|
+
SECRET_SCAN_CAPS = {
|
|
163
|
+
value_scan_cap: 262_144, # 256 KiB (design §V.5.4) — oversize ⇒ anchor-only decision
|
|
164
|
+
pattern_max_matches_per_value: 1000, # match flood ⇒ whole value masked
|
|
165
|
+
pem_block_cap: 16_384, # BEGIN…END search window; truncated ⇒ mask to end of value
|
|
166
|
+
h2_token_min: 24,
|
|
167
|
+
h2_token_max: 512,
|
|
168
|
+
query_pair_cap: 256, # C1.3 — overflow remainder masked; never an unbounded split (F-JS2 class)
|
|
169
|
+
key_len_cap: 256, # C-caps — skip super-linear key work past this (F-PY2 class)
|
|
170
|
+
key_token_cap: 32, # C-caps — matched keys are short
|
|
171
|
+
}.freeze
|
|
172
|
+
|
|
173
|
+
# Tier-1 named-provider patterns (design §V.2), byte-consistent with the
|
|
174
|
+
# python core's SECRET_VALUE_RULES. Each rule: lowercase literal :anchors
|
|
175
|
+
# (the regex NEVER runs unless an anchor occurs case-insensitively in the
|
|
176
|
+
# value — the gitleaks keywords pre-filter and the engine's linearity
|
|
177
|
+
# guarantee), a :regex whose group 1 is the secret span to mask, an
|
|
178
|
+
# optional :entropy floor over the candidate, and an optional :filter
|
|
179
|
+
# post-check (:digit / :classmix). No lookbehind, no possessive groups,
|
|
180
|
+
# bounded quantifiers (design §V.5.2 authoring rules — linted by
|
|
181
|
+
# spec/value_secret_layer_spec.rb). private-key-block and putty-private-key
|
|
182
|
+
# are LINEAR SCANS in the engine (design §V.2 normative algorithm), not
|
|
183
|
+
# table regexes.
|
|
184
|
+
SECRET_VALUE_RULES = [
|
|
185
|
+
{
|
|
186
|
+
id: "aws-access-key-id", ci: false, group: 1,
|
|
187
|
+
regex: '\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA)[A-Z2-7]{16})\b',
|
|
188
|
+
anchors: %w[akia asia abia acca agpa aida aroa aipa anpa anva a3t].freeze,
|
|
189
|
+
}.freeze,
|
|
190
|
+
{
|
|
191
|
+
id: "aws-secret-key", ci: true, group: 1, entropy: 3.5,
|
|
192
|
+
regex: '\baws[a-z0-9_ .\-]{0,25}?(?:key|secret|token)[a-z0-9_ .\-]{0,15}?[\'"]?\s{0,3}' \
|
|
193
|
+
'(?:=|:|=>|:=|,)\s{0,3}[\'"]?([A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])',
|
|
194
|
+
anchors: %w[aws].freeze,
|
|
195
|
+
}.freeze,
|
|
196
|
+
{
|
|
197
|
+
id: "gcp-api-key", ci: false, group: 1,
|
|
198
|
+
regex: '\b(AIza[0-9A-Za-z_\-]{35})(?![0-9A-Za-z_\-])',
|
|
199
|
+
anchors: %w[aiza].freeze,
|
|
200
|
+
}.freeze,
|
|
201
|
+
{
|
|
202
|
+
id: "azure-client-secret", ci: false, group: 1, entropy: 3.0,
|
|
203
|
+
regex: '(?:^|[\s\'"`>=:(,])([A-Za-z0-9_~.]{3}[0-9]Q~[A-Za-z0-9_~.\-]{31,34})(?![A-Za-z0-9_~.\-])',
|
|
204
|
+
anchors: %w[q~].freeze,
|
|
205
|
+
}.freeze,
|
|
206
|
+
{
|
|
207
|
+
id: "azure-storage-key", ci: true, group: 1,
|
|
208
|
+
regex: '\baccount[_\-]?key[\'"]?\s{0,3}[:=]\s{0,3}[\'"]?([A-Za-z0-9+/]{86}==)',
|
|
209
|
+
anchors: %w[accountkey account_key account-key].freeze,
|
|
210
|
+
}.freeze,
|
|
211
|
+
{
|
|
212
|
+
id: "github-token", ci: false, group: 1,
|
|
213
|
+
regex: '\b((?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36})(?![A-Za-z0-9_])',
|
|
214
|
+
anchors: %w[ghp_ gho_ ghu_ ghs_ ghr_].freeze,
|
|
215
|
+
}.freeze,
|
|
216
|
+
{
|
|
217
|
+
id: "github-fine-grained-pat", ci: false, group: 1,
|
|
218
|
+
regex: '\b(github_pat_[A-Za-z0-9_]{82})(?![A-Za-z0-9_])',
|
|
219
|
+
anchors: %w[github_pat_].freeze,
|
|
220
|
+
}.freeze,
|
|
221
|
+
{
|
|
222
|
+
id: "gitlab-token", ci: false, group: 1,
|
|
223
|
+
regex: '\b(glpat-[0-9A-Za-z_\-]{20,300}(?:\.[0-9a-z]{2,9})?|gl(?:ptt|rt|cbt)-[0-9A-Za-z_\-]{20,300}|' \
|
|
224
|
+
'GR1348941[0-9A-Za-z_\-]{20})(?![0-9A-Za-z_\-.])',
|
|
225
|
+
anchors: %w[glpat- glptt- glrt- glcbt- gr1348941].freeze,
|
|
226
|
+
}.freeze,
|
|
227
|
+
{
|
|
228
|
+
id: "slack-token", ci: false, group: 1,
|
|
229
|
+
regex: '\b(xox[abopsr]-(?:[0-9]{4,20}-){1,4}[0-9A-Za-z]{8,64}|xapp-[0-9]-[0-9A-Za-z_]{8,32}-[0-9]{8,16}-' \
|
|
230
|
+
'[0-9a-f]{32,96}|xoxe(?:\.xox[bp])?-[0-9]-[0-9A-Za-z\-]{80,250})(?![0-9A-Za-z\-])',
|
|
231
|
+
anchors: %w[xox xapp-].freeze,
|
|
232
|
+
}.freeze,
|
|
233
|
+
{
|
|
234
|
+
id: "slack-webhook", ci: true, group: 1,
|
|
235
|
+
regex: '(https://hooks\.slack\.com/(?:services|workflows|triggers)/[A-Za-z0-9+/]{20,120})(?![A-Za-z0-9+/])',
|
|
236
|
+
anchors: %w[hooks.slack.com].freeze,
|
|
237
|
+
}.freeze,
|
|
238
|
+
{
|
|
239
|
+
id: "stripe-secret-key", ci: false, group: 1,
|
|
240
|
+
regex: '\b((?:sk|rk)_(?:test|live|prod)_[a-zA-Z0-9]{10,99})(?![a-zA-Z0-9])',
|
|
241
|
+
anchors: %w[sk_test sk_live sk_prod rk_test rk_live rk_prod].freeze,
|
|
242
|
+
}.freeze,
|
|
243
|
+
{
|
|
244
|
+
id: "jwt", ci: false, group: 1,
|
|
245
|
+
regex: '\b(ey[A-Za-z0-9]{17,512}\.ey[A-Za-z0-9/_\-]{17,8192}\.(?:[A-Za-z0-9/_\-]{10,1024}={0,2})?)' \
|
|
246
|
+
'(?![A-Za-z0-9/_\-.=])',
|
|
247
|
+
anchors: %w[eyj].freeze,
|
|
248
|
+
}.freeze,
|
|
249
|
+
{
|
|
250
|
+
id: "uri-userinfo-credentials", ci: true, group: 1,
|
|
251
|
+
regex: '\b[a-z][a-z0-9+.\-]{0,30}://([^/\s:@?#]{1,128}:[^/\s@]{1,256})@',
|
|
252
|
+
anchors: %w[://].freeze,
|
|
253
|
+
}.freeze,
|
|
254
|
+
{
|
|
255
|
+
id: "bearer-token", ci: true, group: 1, filter: :digit,
|
|
256
|
+
regex: '\bbearer\s{1,4}([A-Za-z0-9._~+/=\-]{16,512})(?![A-Za-z0-9._~+/=\-])',
|
|
257
|
+
anchors: %w[bearer].freeze,
|
|
258
|
+
}.freeze,
|
|
259
|
+
{
|
|
260
|
+
id: "basic-auth-value", ci: true, group: 1, filter: :classmix,
|
|
261
|
+
regex: '\bbasic\s{1,4}([A-Za-z0-9+/]{12,512}={0,2})(?![A-Za-z0-9+/=])',
|
|
262
|
+
anchors: %w[basic].freeze,
|
|
263
|
+
}.freeze,
|
|
264
|
+
{
|
|
265
|
+
id: "anthropic-api-key", ci: false, group: 1,
|
|
266
|
+
regex: '\b(sk-ant-(?:admin|api)0[0-9]-[A-Za-z0-9_\-]{80,120}AA)(?![A-Za-z0-9_\-])',
|
|
267
|
+
anchors: %w[sk-ant-].freeze,
|
|
268
|
+
}.freeze,
|
|
269
|
+
{
|
|
270
|
+
id: "openai-api-key", ci: false, group: 1,
|
|
271
|
+
regex: '\b(sk-(?:proj|svcacct|admin)-[A-Za-z0-9_\-]{40,120}T3BlbkFJ[A-Za-z0-9_\-]{40,120}|' \
|
|
272
|
+
'sk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20})(?![A-Za-z0-9_\-])',
|
|
273
|
+
anchors: %w[t3blbkfj].freeze,
|
|
274
|
+
}.freeze,
|
|
275
|
+
].freeze
|
|
276
|
+
|
|
277
|
+
# Anchors for the two linear-scan rules (design §V.2: private-key-block —
|
|
278
|
+
# BEGIN…PRIVATE KEY blocks incl. RSA/EC/DSA/OPENSSH/ENCRYPTED/PGP, truncated
|
|
279
|
+
# block ⇒ mask to end of value, PUBLIC KEY/CERTIFICATE never match; and
|
|
280
|
+
# putty-private-key — the PPK header carries Private-Lines inline ⇒ whole
|
|
281
|
+
# value masked). Folded into the single anchor pre-filter so an OVERSIZED
|
|
282
|
+
# value carrying only a PEM block still fails closed.
|
|
283
|
+
SECRET_LINEAR_SCAN_ANCHORS = ["-----begin", "putty-user-key-file"].freeze
|
|
284
|
+
|
|
285
|
+
# Tier-2 heuristics (design §V.3) — the ONLY value-layer piece with the
|
|
286
|
+
# documented opt-out (secret_heuristics: false). H1 = gitleaks
|
|
287
|
+
# generic-api-key (MIT): keyword-anchored assignment, entropy-gated,
|
|
288
|
+
# four-layer FP suppression. H2 = detect-secrets Base64HighEntropyString
|
|
289
|
+
# (Apache-2.0): whole-value single token only.
|
|
290
|
+
SECRET_HEURISTIC_H1 = {
|
|
291
|
+
id: "generic-api-key", ci: true, group: 1, entropy: 3.5, entropy_hex: 3.0,
|
|
292
|
+
regex: '[\w.\-]{0,50}?(?:access|auth|api|credential|creds|key|passw(?:or)?d|secret|token)' \
|
|
293
|
+
'(?:[ \t\w.\-]{0,20})[\s\'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[`\'"\s=]{0,5}' \
|
|
294
|
+
'([\w.=\-]{10,150}|[a-z0-9][a-z0-9+/]{11,149}={0,3})(?:[`\'"\s;]|\\\\[nr]|$)',
|
|
295
|
+
anchors: %w[access auth api credential creds key passw secret token].freeze,
|
|
296
|
+
}.freeze
|
|
297
|
+
|
|
298
|
+
# H2 anchors are \A/\z (whole string) — Ruby's ^/$ are LINE anchors, so the
|
|
299
|
+
# fixture's ^…$ whole-value semantics translate to \A…\z here (recorded
|
|
300
|
+
# portability translation, design §V.2 conventions).
|
|
301
|
+
SECRET_HEURISTIC_H2 = {
|
|
302
|
+
id: "high-entropy-token", entropy: 4.5,
|
|
303
|
+
token_regex: '\A[A-Za-z0-9+/_\-]{24,512}={0,2}\z',
|
|
304
|
+
uuid_regex: '\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z',
|
|
305
|
+
hex_regex: '\A[0-9a-fA-F]+\z',
|
|
306
|
+
}.freeze
|
|
307
|
+
|
|
308
|
+
# Placeholder shapes (design §V.4, gitleaks global allowlist): a HEURISTIC
|
|
309
|
+
# candidate matching any of these whole is never masked (env refs, template
|
|
310
|
+
# vars, printf specs, repeated fillers). Applied to H1/H2 candidates only —
|
|
311
|
+
# NEVER to Tier-1 matches (no Tier-1 value allowlist: a value allowlist is
|
|
312
|
+
# a bypass shape; AKIAIOSFODNN7EXAMPLE is deliberately a POSITIVE).
|
|
313
|
+
SECRET_VALUE_PLACEHOLDERS = [
|
|
314
|
+
'\A(?:true|false|null|nil|none|undefined)\z',
|
|
315
|
+
'\A(.)\1{3,}\z',
|
|
316
|
+
'\A\$[A-Za-z_][A-Za-z0-9_]*\z',
|
|
317
|
+
'\A\$\{[^}]{0,64}\}\z',
|
|
318
|
+
'\A\{\{[^}]{0,64}\}\}\z',
|
|
319
|
+
'\A%[A-Za-z_]{1,64}%\z',
|
|
320
|
+
'\A@[A-Za-z_]{1,64}@\z',
|
|
321
|
+
'\A%[sdifv]\z',
|
|
322
|
+
'\A<[^>]{0,64}>\z',
|
|
323
|
+
].freeze
|
|
324
|
+
|
|
325
|
+
# Key-context allowlist (design §V.3 / H1): when the matched assignment's
|
|
326
|
+
# KEY text is one of these non-secret names, the H1 candidate is NOT
|
|
327
|
+
# masked. Vendored subset of the gitleaks generic-rule allowlist,
|
|
328
|
+
# byte-consistent with the python core.
|
|
329
|
+
SECRET_KEY_CONTEXT_ALLOWLIST = %w[
|
|
330
|
+
api_version apiversion key_id keyid public_key publickey
|
|
331
|
+
csrf_token primary_key partition_key sort_key idempotency_key
|
|
332
|
+
cache_key routing_key aggregate_key
|
|
333
|
+
].freeze
|
|
334
|
+
|
|
335
|
+
# Stopwords (design §V.4, from the gitleaks curated list): a heuristic
|
|
336
|
+
# candidate CONTAINING any of these is rejected as a placeholder false
|
|
337
|
+
# positive. Applied to H1/H2 candidates only — never to Tier-1 matches.
|
|
338
|
+
# Byte-consistent with the python core's vendored subset; the full
|
|
339
|
+
# 1,436-entry list ships fleet-side in contract/secret-patterns.json.
|
|
340
|
+
SECRET_VALUE_STOPWORDS = %w[
|
|
341
|
+
example placeholder redacted changeme changeit
|
|
342
|
+
yourkey your_api_key your-api-key xxxxxxxx todo
|
|
343
|
+
notreal loremipsum deadbeef 0000000000 1111111111
|
|
344
|
+
].freeze
|
|
135
345
|
end
|
|
136
346
|
end
|