Skip to main content
Any SafeDep Cloud table you can query with safedep query exec can drive an alert. The recipe stays the same for every destination:
  1. Write the SQL for the events you care about.
  2. Run safedep query exec -o json to get rows.
  3. Format the rows into your destination’s payload shape.
  4. POST to the webhook.
The rest of this page is a worked example: PMG block events, formatted as a Slack message, sent to a Slack Incoming Webhook, every 5 minutes. Everything below is a stand-in: swap the query, the formatter, or the curl target for your own destination.

Example: Slack alert for PMG blocks

Every 5 minutes, a scheduled job queries SafeDep Cloud for packages PMG blocked in the last 5 minutes and posts a Slack message like:
Both the package and endpoint are clickable: the package links to its SafeDep community report, the endpoint links to its page in SafeDep Cloud.

Prerequisites

You always need: For the Slack + Python example below, you also need: Confirm the CLI can reach SafeDep Cloud:

Step 1: Write the query

Save the SQL to a file so you can version it in git and rerun it:
blocks.sql
Two things to notice:
  • package_action filter. PMG_PACKAGE_ACTION_BLOCKED is a malicious-package block. PMG_PACKAGE_ACTION_COOLDOWN_BLOCKED is a cooldown-window block (packages held back until they age past your cooldown threshold).
  • JOIN endpoints. Each block event was produced by a PMG invocation on some developer machine or CI runner. Joining endpoints gives you the endpoint’s human-readable name (endpoints.identifier) and its ID (endpoints.id, used to build the Cloud deep-link). The ON clause is a required placeholder: SafeDep Cloud applies the real join from its catalog. See the SQL reference.
Run it once to see the shape of the data:
Enum columns (like package_action and package_ecosystem) return numeric ordinals in JSON; the formatter decodes them in Step 2. For the full schema, tables, and enum values, see the SafeDep Cloud SQL reference.

Step 2: Format events for Slack

Slack’s Incoming Webhook takes a JSON payload with a blocks array. The script below reads rows from stdin, decodes the enum ordinals, builds one Slack section block per event, and prints the payload on stdout. Save this as format.py:
format.py
Add ecosystems you use to the ECOSYSTEM dict; nothing else needs to change.

Step 3: Post to Slack

Pipe the query into the formatter and the formatter into curl:
If there are no matching events, format.py exits with empty stdout and curl sends nothing. On a successful post, Slack replies with ok.
Slack caps a message at 50 blocks, so keep --limit around 10. See Slack’s block limits.

Step 4: Run it on a schedule

To make this an alerting pipeline, filter to a rolling window and run periodically. The window and the schedule interval must match so events aren’t dropped or duplicated. Save this as safedep-alerts.sh next to format.py. It’s the same query as blocks.sql, plus one extra AND clause for the time window, an explicit PATH (schedulers run with a minimal PATH), and a skip when the window has no events (avoids Slack’s invalid_payload on empty POSTs).
safedep-alerts.sh
Then wire it up with your OS’s native scheduler:
On macOS, cron can’t reach the keychain where safedep auth login stores its OAuth token, so scheduled safedep query exec calls fail with not authenticated. Use a launchd user agent instead: it runs in your logged-in user session and inherits keychain access.Save this as ~/Library/LaunchAgents/io.safedep.alerts.plist:
io.safedep.alerts.plist
Load, verify, and tail the log:
RunAtLoad fires the job immediately. StartInterval is in seconds (300 = 5 minutes) and must match the -v-5M window in the script.To stop it:
The design is stateless: no cursor file to keep in sync, no drift across restarts. --limit 10 is set for Slack’s 50-block ceiling. Events beyond the limit in a single window are silently dropped. For burstier traffic, raise --limit (the CLI allows up to 100) and page through the JSON next_page_token, sending one Slack message per page.

Send to other destinations

Only the last two things change: the payload shape (in format.py) and the URL (in curl). Any JSON-accepting HTTP endpoint works.

Discord

Emit a simple content payload and POST to your channel’s webhook URL.

Microsoft Teams

Create a channel Workflow with a webhook trigger and POST an Adaptive Card payload to its URL.

PagerDuty

Emit an Events API v2 payload and POST to the enqueue endpoint.

Query other events

Swap blocks.sql for any question you can ask SafeDep Cloud:
  • Insecure bypasses: filter package_guard_events on event_type = 'PMG_EVENT_TYPE_INSECURE_BYPASS'.
  • Endpoint activity: JOIN endpoints and group by endpoints.identifier to see which machines produced the most events.
  • Malicious packages across projects: query component_malicious_packages with is_verified = true.
See the SafeDep Cloud SQL guide for the full schema, query syntax, and the paging model.