Skip to content

Add JA4 support#1428

Open
skyfallwastaken wants to merge 3 commits into
mainfrom
ja4
Open

Add JA4 support#1428
skyfallwastaken wants to merge 3 commits into
mainfrom
ja4

Conversation

@skyfallwastaken

@skyfallwastaken skyfallwastaken commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary of the problem

Describe your changes

Screenshots / Media


JA4 is a trademark of FoxIO, Inc.

@greptile-apps

greptile-apps Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds support for storing Cloudflare JA4 TLS fingerprints alongside heartbeats to help admins identify requests with spoofed user-agents. When a CF-JA4 header is present, the fingerprint is deduplicated into a ja4s lookup table and the heartbeat records a ja4_id foreign key.

  • New Ja4 model and ja4s table: fingerprints are normalised and upserted via create_or_find_by! so the same TLS client shares a single row across all its heartbeats. The migration uses a concurrent index on heartbeats.ja4_id and deferred FK validation to avoid locking the large table.
  • Admin API updated: user_heartbeats joins ja4s via left_joins and plucks ja4s.fingerprint alongside existing columns; the Swagger spec and RSwag spec are updated accordingly.
  • Deduplication is unaffected: ja4_id is intentionally absent from Heartbeat.indexed_attributes, so the fields_hash used for duplicate detection is unchanged.

Confidence Score: 4/5

Safe to merge; the change is additive, the migration is non-locking, and deduplication logic is untouched.

The implementation is consistent with existing patterns (cf. ip_address via CF-Connecting-IP), the migration correctly defers FK validation and uses a concurrent index, and create_or_find_by! is the right tool for thread-safe fingerprint upserts. The only minor gap is a missing model-level uniqueness validation on Ja4, which leaves direct Ja4.create! calls outside the resolve path without a clean validation error.

app/models/ja4.rb — could benefit from a uniqueness validation to complement the DB constraint.

Important Files Changed

Filename Overview
app/models/ja4.rb New model for JA4 fingerprints with create_or_find_by! upsert logic; missing model-level uniqueness validation but DB constraint is enforced.
app/services/heartbeat_ingest.rb Adds resolved_ja4 memoization (instance-scoped, correct for per-request batches) and ja4_id to normalized attrs; dedup hash is unaffected since indexed_attributes doesn't include ja4_id.
db/migrate/20260609195005_create_ja4s_and_add_ja4_to_heartbeats.rb Migration correctly uses disable_ddl_transaction! for the concurrent heartbeats index, deferred FK validation, and on_delete: :nullify for safe cascades.
app/controllers/concerns/api/admin/v1/user_utilities.rb Adds ja4s.fingerprint to HEARTBEAT_RESPONSE_COLUMNS as a raw SQL string and uses left_joins(:ja4) in the pluck query; correctly destructures the new column in the map block.
app/models/heartbeat.rb Adds optional belongs_to :ja4; ja4_id is excluded from indexed_attributes so existing deduplication (fields_hash) is unaffected.

Sequence Diagram

sequenceDiagram
    participant Client
    participant CF as Cloudflare
    participant Controller as HackatimeController
    participant Ingest as HeartbeatIngest
    participant Ja4Model as Ja4.resolve
    participant DB

    Client->>CF: POST /heartbeats (TLS handshake)
    CF->>Controller: Request + CF-JA4 header
    Controller->>Ingest: "call(request_context: { ja4: t13d... })"
    Ingest->>Ja4Model: Ja4.resolve(fingerprint)
    Ja4Model->>DB: create_or_find_by!(fingerprint:)
    DB-->>Ja4Model: Ja4 record (new or existing)
    Ja4Model-->>Ingest: "@resolved_ja4"
    Ingest->>DB: Heartbeat.insert(attrs + ja4_id:)
    DB-->>Ingest: persisted heartbeat
    Ingest-->>Controller: Result
    Controller-->>Client: 202 Accepted
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
app/models/ja4.rb:4
**Missing model-level uniqueness validation**

`Ja4` only validates `presence` but not uniqueness at the model layer. `create_or_find_by!` correctly handles the DB-level constraint for the `resolve` path, but any direct `Ja4.create!(fingerprint: x)` call (e.g. in tests or future code) will hit a raw `ActiveRecord::RecordNotUnique` rather than a clear validation error. Adding `validates :fingerprint, uniqueness: true` makes the constraint explicit and produces friendlier messages outside the `resolve` path.

Reviews (1): Last reviewed commit: "Add JA4 support" | Re-trigger Greptile

Comment thread app/models/ja4.rb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant