cortexdb-connectors 0.2.12__tar.gz → 0.2.13__tar.gz
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.
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/PKG-INFO +1 -1
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/base.py +1 -1
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/webhooks.py +87 -19
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors.egg-info/PKG-INFO +1 -1
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/pyproject.toml +1 -1
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_jira_findings_0_2_9.py +1 -1
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_tldv_findings_0_2_12.py +87 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_webhooks_replay.py +40 -12
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/README.md +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/cli.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/confluence/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/discord/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/freshdesk/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/github/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/gitlab/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/google_workspace/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/hubspot/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/insights/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/insights/api.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/insights/detectors.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/insights/engine.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/intercom/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/jira/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/linear/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/notion/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/pagerduty/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/salesforce/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/servicenow/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/slack/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/state.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/teams/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/tldv/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/worker.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/zendesk/__init__.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors.egg-info/SOURCES.txt +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors.egg-info/dependency_links.txt +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors.egg-info/entry_points.txt +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors.egg-info/requires.txt +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors.egg-info/top_level.txt +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/setup.cfg +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_base.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_connector_coverage.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_github.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_ingest_e2e.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_jira_attachments.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_jira_findings_0_2_11.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_product_findings_0_2_12.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_slack.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_streaming_cursor.py +0 -0
- {cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_webhooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cortexdb-connectors
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: Data connectors for CortexDB — ingest from Slack, GitHub, GitLab, Jira, Linear, Confluence, Notion, PagerDuty, Discord, Teams, Google Workspace, Salesforce, HubSpot, Zendesk, Intercom, and ServiceNow into the v1 memory API.
|
|
5
5
|
Author-email: CortexDB Team <team@cortexdb.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -536,13 +536,17 @@ def _verify_hmac_sha256(secret: str, body: bytes, signature: str) -> bool:
|
|
|
536
536
|
|
|
537
537
|
|
|
538
538
|
class _HmacWebhookHandler:
|
|
539
|
-
"""Shared base for
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
539
|
+
"""Shared base for provider webhook receivers (Jira, Freshdesk, tl;dv).
|
|
540
|
+
|
|
541
|
+
Authentication is per-provider via ``verify()``: the default checks an
|
|
542
|
+
HMAC-SHA256 body signature (``sha256=<hex>``) under ``signature_header``
|
|
543
|
+
— but each provider names its own header (Jira signs under
|
|
544
|
+
``X-Hub-Signature``, not GitHub's ``X-Hub-Signature-256``), and some
|
|
545
|
+
don't body-sign at all (see ``_StaticHeaderWebhookHandler``), so
|
|
546
|
+
subclasses override the header name or the whole scheme. Everything
|
|
547
|
+
after auth is scheme-agnostic and shared: parse JSON, redact on delete
|
|
548
|
+
events, else call the subclass ``normalize`` (which may return ``None``
|
|
549
|
+
to skip an irrelevant event) and ingest via ``POST /v1/experience``.
|
|
546
550
|
"""
|
|
547
551
|
|
|
548
552
|
signature_header = "X-Hub-Signature-256"
|
|
@@ -562,11 +566,18 @@ class _HmacWebhookHandler:
|
|
|
562
566
|
self.scope = scope
|
|
563
567
|
self.actor = actor or os.environ.get("CORTEXDB_ACTOR", "")
|
|
564
568
|
|
|
569
|
+
def verify(self, headers: Any, body: bytes) -> bool:
|
|
570
|
+
"""True when the delivery is authentic. Default: HMAC-SHA256 body
|
|
571
|
+
signature under ``signature_header``. Providers with a different
|
|
572
|
+
auth scheme override this; ``handle()`` is scheme-agnostic."""
|
|
573
|
+
return _verify_hmac_sha256(
|
|
574
|
+
self.secret, body, headers.get(self.signature_header, "")
|
|
575
|
+
)
|
|
576
|
+
|
|
565
577
|
async def handle(self, request: Request) -> JSONResponse:
|
|
566
578
|
body = await request.body()
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
logger.warning("%s signature verification failed", self.provider)
|
|
579
|
+
if not self.verify(request.headers, body):
|
|
580
|
+
logger.warning("%s webhook authentication failed", self.provider)
|
|
570
581
|
return JSONResponse({"error": "invalid signature"}, status_code=401)
|
|
571
582
|
try:
|
|
572
583
|
payload = json.loads(body)
|
|
@@ -638,6 +649,29 @@ class _HmacWebhookHandler:
|
|
|
638
649
|
return None
|
|
639
650
|
|
|
640
651
|
|
|
652
|
+
class _StaticHeaderWebhookHandler(_HmacWebhookHandler):
|
|
653
|
+
"""Auth for providers that cannot HMAC-sign the body (T47).
|
|
654
|
+
|
|
655
|
+
tl;dv and Freshdesk send a static, user-configured header with every
|
|
656
|
+
delivery instead of a body signature — configure the provider to send
|
|
657
|
+
``auth_header`` with the shared secret as its value (a ``Bearer ``
|
|
658
|
+
prefix is accepted) and set the same secret on the handler. Fail-closed:
|
|
659
|
+
with no secret configured every delivery is rejected, same as the HMAC
|
|
660
|
+
path. Static-header auth carries no body integrity or replay binding —
|
|
661
|
+
it is what these providers offer, not an equivalent to a signature.
|
|
662
|
+
"""
|
|
663
|
+
|
|
664
|
+
auth_header = "Authorization"
|
|
665
|
+
|
|
666
|
+
def verify(self, headers: Any, body: bytes) -> bool:
|
|
667
|
+
if not self.secret:
|
|
668
|
+
return False
|
|
669
|
+
supplied = headers.get(self.auth_header, "")
|
|
670
|
+
if supplied.startswith("Bearer "):
|
|
671
|
+
supplied = supplied[len("Bearer "):]
|
|
672
|
+
return bool(supplied) and hmac.compare_digest(supplied, self.secret)
|
|
673
|
+
|
|
674
|
+
|
|
641
675
|
def _parse_iso(value: Any) -> datetime:
|
|
642
676
|
"""Parse an ISO-8601 string (Z or offset) → aware datetime; fall back to now.
|
|
643
677
|
|
|
@@ -659,6 +693,11 @@ class JiraWebhookHandler(_HmacWebhookHandler):
|
|
|
659
693
|
"""Jira Cloud webhook → Episode. Configure the webhook with an HMAC secret."""
|
|
660
694
|
|
|
661
695
|
provider = "jira"
|
|
696
|
+
# T47-family: Jira signs the body with the same sha256=<hex> HMAC scheme
|
|
697
|
+
# but sends it under WebSub-style X-Hub-Signature, not GitHub's
|
|
698
|
+
# X-Hub-Signature-256 — reading the wrong header 401'd every correctly
|
|
699
|
+
# signed delivery.
|
|
700
|
+
signature_header = "X-Hub-Signature"
|
|
662
701
|
|
|
663
702
|
def deletion_entity(self, payload: dict[str, Any]) -> str | None:
|
|
664
703
|
# C2: jira:issue_deleted → redact the issue (its key is what the
|
|
@@ -718,8 +757,11 @@ class JiraWebhookHandler(_HmacWebhookHandler):
|
|
|
718
757
|
)
|
|
719
758
|
|
|
720
759
|
|
|
721
|
-
class FreshdeskWebhookHandler(
|
|
722
|
-
"""Freshdesk
|
|
760
|
+
class FreshdeskWebhookHandler(_StaticHeaderWebhookHandler):
|
|
761
|
+
"""Freshdesk automation webhook → Episode.
|
|
762
|
+
|
|
763
|
+
Freshdesk automations don't HMAC-sign the body; auth is a custom header
|
|
764
|
+
configured on the automation rule (static-header scheme, T47-family)."""
|
|
723
765
|
|
|
724
766
|
provider = "freshdesk"
|
|
725
767
|
|
|
@@ -754,8 +796,13 @@ class FreshdeskWebhookHandler(_HmacWebhookHandler):
|
|
|
754
796
|
)
|
|
755
797
|
|
|
756
798
|
|
|
757
|
-
class TldvWebhookHandler(
|
|
758
|
-
"""tl;dv webhook (
|
|
799
|
+
class TldvWebhookHandler(_StaticHeaderWebhookHandler):
|
|
800
|
+
"""tl;dv webhook (MeetingReady / TranscriptReady) → Episode.
|
|
801
|
+
|
|
802
|
+
tl;dv does not HMAC-sign deliveries — its webhook auth is a static
|
|
803
|
+
custom header configured in Settings → Webhooks (T47), so this uses the
|
|
804
|
+
static-header scheme: configure tl;dv to send
|
|
805
|
+
``Authorization: Bearer <TLDV_WEBHOOK_SECRET>``.
|
|
759
806
|
|
|
760
807
|
Builds its episode through the SAME ``build_meeting_episode`` as the
|
|
761
808
|
poll connector (T3): same content shape, same versioned idempotency key
|
|
@@ -788,7 +835,26 @@ class TldvWebhookHandler(_HmacWebhookHandler):
|
|
|
788
835
|
return None
|
|
789
836
|
|
|
790
837
|
def normalize(self, payload: dict[str, Any]) -> Episode | None:
|
|
791
|
-
from cortexdb_connectors.tldv import build_meeting_episode
|
|
838
|
+
from cortexdb_connectors.tldv import build_meeting_episode, parse_tldv_ts
|
|
839
|
+
|
|
840
|
+
# T46: TranscriptReady's `data` is a transcript envelope
|
|
841
|
+
# {id: <transcriptId>, meetingId, data: [segments]} — its `id` is the
|
|
842
|
+
# TRANSCRIPT id, so letting _extract_meeting treat it as the meeting
|
|
843
|
+
# mis-keys the episode and drops the segments. Key by the meeting id
|
|
844
|
+
# and feed the envelope through as the transcript instead.
|
|
845
|
+
data = payload.get("data")
|
|
846
|
+
if isinstance(data, dict) and data.get("meetingId"):
|
|
847
|
+
return build_meeting_episode(
|
|
848
|
+
{"id": data["meetingId"]},
|
|
849
|
+
data,
|
|
850
|
+
self.scope,
|
|
851
|
+
occurred_at=parse_tldv_ts(payload.get("executedAt")),
|
|
852
|
+
raw_payload=payload,
|
|
853
|
+
extra_metadata={
|
|
854
|
+
"event": payload.get("event", ""),
|
|
855
|
+
"transcript_id": data.get("id"),
|
|
856
|
+
},
|
|
857
|
+
)
|
|
792
858
|
|
|
793
859
|
meeting = self._extract_meeting(payload)
|
|
794
860
|
if meeting is None:
|
|
@@ -803,7 +869,7 @@ class TldvWebhookHandler(_HmacWebhookHandler):
|
|
|
803
869
|
return None
|
|
804
870
|
return build_meeting_episode(
|
|
805
871
|
meeting,
|
|
806
|
-
None, #
|
|
872
|
+
None, # MeetingReady-style deliveries carry no transcript
|
|
807
873
|
self.scope,
|
|
808
874
|
raw_payload=payload,
|
|
809
875
|
extra_metadata={"event": payload.get("event", "")},
|
|
@@ -973,9 +1039,11 @@ class WebhookServer:
|
|
|
973
1039
|
scope=self.scope,
|
|
974
1040
|
actor=self.actor,
|
|
975
1041
|
)
|
|
976
|
-
#
|
|
977
|
-
#
|
|
978
|
-
#
|
|
1042
|
+
# Handlers for the Limechat pilot sources. Each reads its own
|
|
1043
|
+
# *_WEBHOOK_SECRET; auth matches what the provider actually sends —
|
|
1044
|
+
# Jira signs the body (sha256=<hex> under X-Hub-Signature), while
|
|
1045
|
+
# tl;dv and Freshdesk send the secret as a static header
|
|
1046
|
+
# (Authorization: Bearer <secret>).
|
|
979
1047
|
self.jira_handler = JiraWebhookHandler(
|
|
980
1048
|
secret=os.environ.get("JIRA_WEBHOOK_SECRET", ""),
|
|
981
1049
|
cortexdb_url=self.cortexdb_url,
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cortexdb-connectors
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: Data connectors for CortexDB — ingest from Slack, GitHub, GitLab, Jira, Linear, Confluence, Notion, PagerDuty, Discord, Teams, Google Workspace, Salesforce, HubSpot, Zendesk, Intercom, and ServiceNow into the v1 memory API.
|
|
5
5
|
Author-email: CortexDB Team <team@cortexdb.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cortexdb-connectors"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.13"
|
|
8
8
|
description = "Data connectors for CortexDB — ingest from Slack, GitHub, GitLab, Jira, Linear, Confluence, Notion, PagerDuty, Discord, Teams, Google Workspace, Salesforce, HubSpot, Zendesk, Intercom, and ServiceNow into the v1 memory API."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_tldv_findings_0_2_12.py
RENAMED
|
@@ -264,3 +264,90 @@ def test_jira_webhook_flattens_adf_description():
|
|
|
264
264
|
)
|
|
265
265
|
assert ep is not None
|
|
266
266
|
assert "the actual body" in ep.content
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ── T46: TranscriptReady keyed by meeting id, transcript kept ────────────────
|
|
270
|
+
|
|
271
|
+
_TRANSCRIPT_READY = {
|
|
272
|
+
"event": "TranscriptReady",
|
|
273
|
+
"data": {
|
|
274
|
+
"id": "TRANSCRIPT-999",
|
|
275
|
+
"meetingId": "MEETING-123",
|
|
276
|
+
"data": [{"startTime": 0, "endTime": 18, "speaker": "A", "text": "the real transcript"}],
|
|
277
|
+
},
|
|
278
|
+
"id": "web-job-x",
|
|
279
|
+
"executedAt": "2026-07-29T10:27:14.729Z",
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _tldv_handler():
|
|
284
|
+
from cortexdb_connectors.webhooks import TldvWebhookHandler
|
|
285
|
+
|
|
286
|
+
return TldvWebhookHandler(
|
|
287
|
+
secret="whsec_test", cortexdb_url="u", cortexdb_api_key="k", scope="s", actor="a"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_transcript_ready_keys_by_meeting_id_and_keeps_transcript():
|
|
292
|
+
ep = _tldv_handler().normalize(_TRANSCRIPT_READY)
|
|
293
|
+
assert ep is not None
|
|
294
|
+
# Keyed by the MEETING id, never the transcript id.
|
|
295
|
+
assert ep.entities[0].entity_id == "MEETING-123"
|
|
296
|
+
assert ep.idempotency_key.startswith("tldv:MEETING-123:")
|
|
297
|
+
assert "TRANSCRIPT-999" not in ep.idempotency_key
|
|
298
|
+
# The segments land in the ingested content.
|
|
299
|
+
assert "A: the real transcript" in ep.content
|
|
300
|
+
# Chronology from the delivery's executedAt, not now().
|
|
301
|
+
assert ep.occurred_at == datetime(2026, 7, 29, 10, 27, 14, 729000, tzinfo=timezone.utc)
|
|
302
|
+
assert ep.metadata["transcript_id"] == "TRANSCRIPT-999"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def test_transcript_ready_without_segments_still_keys_by_meeting_id():
|
|
306
|
+
payload = {
|
|
307
|
+
"event": "TranscriptReady",
|
|
308
|
+
"data": {"id": "TRANSCRIPT-1", "meetingId": "MEETING-7"},
|
|
309
|
+
"executedAt": "2026-07-29T10:27:14.729Z",
|
|
310
|
+
}
|
|
311
|
+
ep = _tldv_handler().normalize(payload)
|
|
312
|
+
assert ep is not None
|
|
313
|
+
assert ep.idempotency_key.startswith("tldv:MEETING-7:")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ── T47: provider auth matches what the provider actually sends ──────────────
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_tldv_static_header_auth():
|
|
320
|
+
h = _tldv_handler()
|
|
321
|
+
body = b"{}"
|
|
322
|
+
assert h.verify({"Authorization": "Bearer whsec_test"}, body)
|
|
323
|
+
assert h.verify({"Authorization": "whsec_test"}, body)
|
|
324
|
+
assert not h.verify({"Authorization": "Bearer wrong"}, body)
|
|
325
|
+
assert not h.verify({}, body) # real tl;dv sends no X-Hub-Signature-256
|
|
326
|
+
assert not h.verify({"X-Hub-Signature-256": "sha256=deadbeef"}, body)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_tldv_no_secret_fails_closed():
|
|
330
|
+
from cortexdb_connectors.webhooks import TldvWebhookHandler
|
|
331
|
+
|
|
332
|
+
h = TldvWebhookHandler(
|
|
333
|
+
secret="", cortexdb_url="u", cortexdb_api_key="k", scope="s", actor="a"
|
|
334
|
+
)
|
|
335
|
+
assert not h.verify({"Authorization": "Bearer anything"}, b"{}")
|
|
336
|
+
assert not h.verify({}, b"{}")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_jira_signs_under_x_hub_signature():
|
|
340
|
+
import hashlib
|
|
341
|
+
import hmac as hmac_mod
|
|
342
|
+
|
|
343
|
+
from cortexdb_connectors.webhooks import JiraWebhookHandler
|
|
344
|
+
|
|
345
|
+
h = JiraWebhookHandler(
|
|
346
|
+
secret="s", cortexdb_url="u", cortexdb_api_key="k", scope="s", actor="a"
|
|
347
|
+
)
|
|
348
|
+
body = b'{"webhookEvent": "jira:issue_updated"}'
|
|
349
|
+
sig = "sha256=" + hmac_mod.new(b"s", body, hashlib.sha256).hexdigest()
|
|
350
|
+
# Jira sends WebSub-style X-Hub-Signature — that must verify...
|
|
351
|
+
assert h.verify({"X-Hub-Signature": sig}, body)
|
|
352
|
+
# ...and GitHub's X-Hub-Signature-256 (which Jira never sends) must not.
|
|
353
|
+
assert not h.verify({"X-Hub-Signature-256": sig}, body)
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
"""Webhook replay tests for the
|
|
1
|
+
"""Webhook replay tests for the provider handlers (jira, freshdesk, tldv).
|
|
2
2
|
|
|
3
3
|
Drives the real WebhookServer ASGI app with a Starlette TestClient and
|
|
4
|
-
synthetic provider payloads (built from public sample shapes)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
synthetic provider payloads (built from public sample shapes), using each
|
|
5
|
+
provider's REAL auth scheme (T47): Jira HMAC-signs the body under
|
|
6
|
+
X-Hub-Signature; tl;dv and Freshdesk send a static Authorization header and
|
|
7
|
+
no signature at all. Asserts:
|
|
8
|
+
* bad/missing auth is rejected 401
|
|
9
|
+
* a correctly authenticated delivery is accepted 200 and lands in the sink
|
|
10
|
+
|
|
11
|
+
No live credentials; the only secret is the test's own key.
|
|
9
12
|
"""
|
|
10
13
|
|
|
11
14
|
from __future__ import annotations
|
|
@@ -27,6 +30,20 @@ def _sign(body: bytes) -> str:
|
|
|
27
30
|
return "sha256=" + hmac.new(_SECRET.encode(), body, hashlib.sha256).hexdigest()
|
|
28
31
|
|
|
29
32
|
|
|
33
|
+
def _good_auth(provider: str, body: bytes) -> dict[str, str]:
|
|
34
|
+
"""Each provider's real auth: Jira = body HMAC under X-Hub-Signature;
|
|
35
|
+
tl;dv/Freshdesk = static Authorization header, no signature."""
|
|
36
|
+
if provider == "jira":
|
|
37
|
+
return {"X-Hub-Signature": _sign(body)}
|
|
38
|
+
return {"Authorization": f"Bearer {_SECRET}"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _bad_auth(provider: str) -> dict[str, str]:
|
|
42
|
+
if provider == "jira":
|
|
43
|
+
return {"X-Hub-Signature": "sha256=deadbeef"}
|
|
44
|
+
return {"Authorization": "Bearer wrong-secret"}
|
|
45
|
+
|
|
46
|
+
|
|
30
47
|
# provider -> (secret_env_var, route, scope, payload, distinctive content word)
|
|
31
48
|
CASES = {
|
|
32
49
|
"jira": (
|
|
@@ -98,28 +115,39 @@ def _client(provider: str, sink, monkeypatch) -> tuple[TestClient, str, dict, st
|
|
|
98
115
|
|
|
99
116
|
|
|
100
117
|
@pytest.mark.parametrize("provider", list(CASES), ids=list(CASES))
|
|
101
|
-
def
|
|
118
|
+
def test_bad_auth_rejected(provider, cortex_sink, monkeypatch) -> None:
|
|
102
119
|
client, route, payload, _ = _client(provider, cortex_sink, monkeypatch)
|
|
103
120
|
body = json.dumps(payload).encode()
|
|
104
121
|
r = client.post(
|
|
105
122
|
route,
|
|
106
123
|
content=body,
|
|
107
|
-
headers={
|
|
124
|
+
headers={**_bad_auth(provider), "Content-Type": "application/json"},
|
|
125
|
+
)
|
|
126
|
+
assert r.status_code == 401, f"{provider}: bad auth should be 401, got {r.status_code}"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@pytest.mark.parametrize("provider", list(CASES), ids=list(CASES))
|
|
130
|
+
def test_missing_auth_rejected(provider, cortex_sink, monkeypatch) -> None:
|
|
131
|
+
client, route, payload, _ = _client(provider, cortex_sink, monkeypatch)
|
|
132
|
+
r = client.post(
|
|
133
|
+
route,
|
|
134
|
+
content=json.dumps(payload).encode(),
|
|
135
|
+
headers={"Content-Type": "application/json"},
|
|
108
136
|
)
|
|
109
|
-
assert r.status_code == 401, f"{provider}:
|
|
137
|
+
assert r.status_code == 401, f"{provider}: missing auth should be 401, got {r.status_code}"
|
|
110
138
|
|
|
111
139
|
|
|
112
140
|
@pytest.mark.parametrize("provider", list(CASES), ids=list(CASES))
|
|
113
|
-
def
|
|
141
|
+
def test_good_auth_ingests(provider, cortex_sink, monkeypatch) -> None:
|
|
114
142
|
client, route, payload, scope = _client(provider, cortex_sink, monkeypatch)
|
|
115
143
|
word = CASES[provider][4]
|
|
116
144
|
body = json.dumps(payload).encode()
|
|
117
145
|
r = client.post(
|
|
118
146
|
route,
|
|
119
147
|
content=body,
|
|
120
|
-
headers={
|
|
148
|
+
headers={**_good_auth(provider, body), "Content-Type": "application/json"},
|
|
121
149
|
)
|
|
122
|
-
assert r.status_code == 200, f"{provider}: good
|
|
150
|
+
assert r.status_code == 200, f"{provider}: good auth should be 200, got {r.status_code} {r.text}"
|
|
123
151
|
|
|
124
152
|
items = []
|
|
125
153
|
for _ in range(12):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/confluence/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/discord/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/freshdesk/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/github/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/gitlab/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/hubspot/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/insights/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/insights/api.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/insights/detectors.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/insights/engine.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/intercom/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/jira/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/linear/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/notion/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/pagerduty/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/salesforce/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/servicenow/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/slack/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/teams/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/tldv/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors/zendesk/__init__.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors.egg-info/requires.txt
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/cortexdb_connectors.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_jira_findings_0_2_11.py
RENAMED
|
File without changes
|
{cortexdb_connectors-0.2.12 → cortexdb_connectors-0.2.13}/tests/test_product_findings_0_2_12.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|