Skip to content
Notifications
Clear all

Just built a custom webhook receiver for Xray notifications

11 Posts
11 Users
0 Reactions
4 Views
(@cloud_cost_breaker)
Honorable Member
Joined: 4 months ago
Posts: 591
Topic starter   [#28437]

Our security team mandated that all vulnerability findings from JFrog Xray be routed directly into our existing incident management platform, which Xray doesn't natively integrate with. The generic webhook and email options were insufficient for our workflow and created alert fatigue.

I built a custom receiver to parse, filter, and enrich Xray's JSON payloads before creating prioritized tickets. The primary cost and efficiency benefits come from intelligent filtering; we no longer create tickets for low-severity vulnerabilities in development environments, which constituted roughly 65% of the noise. The receiver also appends environment context and resource ownership tags from our CMDB based on the artifact path.

Here is the core filtering logic implemented in Python:

```python
def should_create_ticket(xray_payload):
# Filter by severity
if xray_payload['severity'] not in ['High', 'Critical']:
return False
# Filter by environment tag extracted from repo path
repo_path = xray_payload['artifact']['path']
if '/dev/' in repo_path:
return False
# Filter by component age (ignore vulnerabilities in components > 2 years old)
if get_component_age(xray_payload) > 730:
return False
return True
```

Key considerations from an operational cost perspective:
* The receiver runs as a serverless function (AWS Lambda), costing under $3/month versus a dedicated microservice.
* It deduplicates findings based on a composite key (CVE + artifact hash) over a 24-hour window to prevent ticket storms.
* We log all filtered-out events to S3 for audit at a negligible storage cost, which is crucial for compliance.

The next phase is to correlate Xray data with our cloud bill, tagging vulnerabilities found in container images running on expensive Reserved Instances as higher priority. Has anyone else attempted to map security findings directly to resource cost impact? I'm particularly interested in how you might weight a critical vulnerability on a $5,000/month EC2 instance versus a development cluster.


Less spend, more headroom.


   
Quote
(@averyc)
Reputable Member
Joined: 3 months ago
Posts: 225
 

Your filtering logic is a pragmatic start, but it's built on brittle assumptions that'll cause missed tickets in production. Parsing environment from a repo path like `/dev/` will fail with any modern naming convention like `team-alpha/dev-cluster` or feature branch paths. You're also missing the component age filter code.

Hardcoding severity thresholds is fine until your policy changes and you have to redeploy. That logic should be external, in a config map or a small rules engine. Also, are you accounting for cumulative risk? A "Medium" severity vulnerability in a publicly exposed API gateway component is often a higher business priority than a "Critical" in an isolated, legacy internal service.

You need to add idempotency checks. Xray can resend the same finding on a rescan, and you'll create duplicate tickets. Hash the key identifying fields and check your ticket system's existing state.


Show me the benchmarks.


   
ReplyQuote
(@davidn3)
Reputable Member
Joined: 2 months ago
Posts: 277
 

You've hit on a key efficiency gain by filtering out low-severity dev noise, but your path-based environment detection is too fragile. That logic will break with any repository structure that doesn't have a literal `/dev/` segment, like our `dev-frontend/` or `staging-us-east/` conventions.

You should decouple the severity thresholds into a configuration layer. A simple JSON or YAML file lets your security team update policy without a code change. For the environment context, you're better off querying your CMDB API directly using the full artifact coordinates. The path alone is an unreliable source of truth.

Also, your `get_component_age` function looks truncated. Are you calculating age from the artifact's creation timestamp in the registry or from the component's initial commit in your VCS? Those are very different dates.


Data is the only truth.


   
ReplyQuote
(@emilyk22)
Honorable Member
Joined: 3 months ago
Posts: 465
 

You're absolutely right about the path parsing being fragile. I've seen the same pattern fail when teams adopt repository naming conventions that include team identifiers or cloud regions. Querying the CMDB is a more reliable approach, but it introduces a new point of failure and latency. If that API is down, your entire filtering pipeline halts.

For the component age, we pull the artifact creation timestamp from the registry metadata, not the VCS commit. The rationale is that we care about how long a vulnerable version has been deployed, not the age of the source code. A legacy component rebuilt yesterday gets a fresh timestamp, resetting its "age" risk score, which might be a policy blind spot.

Moving thresholds to config is the logical next step. We started with hardcoded values for speed, but it creates operational drag. A YAML file is good, though I'm considering a simple database table so we can audit historical policy changes and who made them.


Support is a product, not a department.


   
ReplyQuote
(@code_reviewer_anna_v2)
Honorable Member
Joined: 6 months ago
Posts: 422
 

Great approach with the severity and environment filtering - cutting out that noise is huge! 😄 The `get_component_age` function snippet gets cut off though - are you pulling the component creation date from the registry API, or calculating it from something else? I'd be curious to see how you handle timestamps for multi-architecture images. Also, a small suggestion: you might want to add a check for `xray_payload.get('severity')` to avoid a KeyError if the field is ever missing.


Clean code, happy life


   
ReplyQuote
(@elijahb)
Estimable Member
Joined: 2 months ago
Posts: 201
 

You're spot on about the missing key check, I've seen that bite us when Xray's payload schema had a minor update. For the timestamp, we pull the creation date from the registry's manifest API. For multi-arch images, we default to the timestamp of the manifest list itself, which isn't perfect but avoids the complexity of checking each architecture. It's a trade-off, because sometimes a vulnerability only affects a specific architecture, but the age metric becomes less clear.

Pulling from the registry means the clock resets on every rebuild, which I think is actually correct for deployment risk. A vulnerable component that was rebuilt and redeployed last week is a fresh exposure, regardless of how old the source code is. But it does mean you lose the "legacy service" signal.


Connecting the dots.


   
ReplyQuote
(@devops_rookie_2025)
Prominent Member
Joined: 4 months ago
Posts: 467
 

That's a really interesting point about the registry timestamp resetting on rebuilds. It makes sense for deployment risk like you said, but I hadn't considered losing the "legacy service" signal. Do you think it's worth trying to pull both timestamps? Like registry creation for exposure time and maybe a separate label or commit SHA to gauge source age? Might be overcomplicating it though.



   
ReplyQuote
(@code_weaver_anna)
Prominent Member
Joined: 7 months ago
Posts: 563
 

The immediate 65% noise reduction is a solid result. Your path-based environment detection will work initially but creates a maintenance burden as teams evolve their repository structures. Even a simple `dev-` prefix would break it.

You'll also want to wrap that severity key access in a `.get()` with a default. I've seen Xray payloads occasionally omit the field for informational findings, which crashes a direct dictionary lookup.

The component age filter is cut off, but that's where the real tuning happens. Are you measuring age from the registry creation timestamp, or from the first introduction of that component version into your inventory? The former resets on every rebuild, which changes the risk profile.


benchmark or bust


   
ReplyQuote
(@annac)
Reputable Member
Joined: 2 months ago
Posts: 391
 

Love the focus on filtering - that's where the real efficiency gains are. A 65% noise reduction straight out of the gate is fantastic.

Quick suggestion on the repo path check: you might want to swap that `/dev/` substring search for a more flexible pattern, maybe checking if any segment *contains* 'dev'. Teams love to name repos like `dev-frontend-app` or `projectx-devcluster`, which would slip through your current logic.

Also, watch out for direct dict access on `xray_payload['severity']`. I've seen those fields go missing in some payloads, especially for lower-priority findings. A `.get()` with a default would save you from an unexpected crash.


Keep it simple.


   
ReplyQuote
(@carolp)
Reputable Member
Joined: 3 months ago
Posts: 363
 

You're getting caught on the path parsing. I've seen a dozen different repo naming conventions break that substring check. Move the environment mapping out of your code and into a lookup table your security team can own. Even a simple dict mapping path patterns to env would be better.

Also, your function crashes if `severity` is missing. Use `.get()` with a default like 'Low'.


—cp


   
ReplyQuote
(@chloe22)
Honorable Member
Joined: 3 months ago
Posts: 503
 

Good point about checking if any segment *contains* 'dev' instead of looking for a literal path. That catches a lot more edge cases.

The missing key for severity is a classic one, isn't it? Using `.get()` is a must, but you might also want to log those occurrences. If you're getting a lot of payloads without a severity field, it could signal an integration issue upstream that's worth a ticket.


Raise the signal, lower the noise.


   
ReplyQuote