How to Route Violations to CODEOWNERS-Resolved Owners with ActionNotifyOwner
Fire a notification, not a block — when a policy with `ActionNotifyOwner` matches, Chainsaw resolves the manifest's CODEOWNERS team and dispatches via your configured destination map. The minimum-friction way to put accountability on the right team.
The Problem ActionNotifyOwner Solves
You can Block a vulnerable dependency. But blocking breaks the build, which means the developer is paged immediately, and the problem becomes urgent regardless of severity. For a CVSS 6.5 advisory in a transitive dep, that’s the wrong shape — you want the team that owns the manifest to know about it with enough lead time to plan a fix, not break their build at 5 PM on a Friday.
ActionNotifyOwner is the right action for the gap between “do nothing” and “block”:
- Detects the violation.
- Resolves the manifest path’s CODEOWNERS entry.
- Looks up the team’s destinations in the per-team map.
- Dispatches a notification (Slack / Teams / PagerDuty / email — whatever the team’s row says).
- Does not block the install.
It is the missing rung between observability and enforcement.
Prerequisites
- Manager or Admin role.
- A populated CODEOWNERS file in the affected repositories.
- Outbound destinations configured — see How to Send HMAC-Signed Outbound Webhooks.
Step 1: Configure the Per-Team Destination Map
Open Admin → Org settings → Ownership. The page has two halves: the CODEOWNERS source (where Chainsaw reads ownership from) and the destination map (where each team’s notifications go).
CODEOWNERS source
Chainsaw resolves ownership in this order:
- The repository’s
.github/CODEOWNERS(orCODEOWNERSat the root, ordocs/CODEOWNERS). - The org-level fallback configured here.
- The default
@security-teamif everything else is empty.
Most setups just point at the repo and rely on per-repo CODEOWNERS files.
Destination map
| Owner | Slack channel | PagerDuty | |
|---|---|---|---|
@frontend-team | #frontend-secops | — | frontend-leads@example.com |
@platform-team | #platform-secops | PD-Schedule-Platform | — |
@ml-team | #ml-supply-chain | — | ml-leads@example.com |
@security-team (fallback) | #security | PD-Schedule-Security | secops@example.com |
The fallback row is the safety net — if Chainsaw can’t resolve a team, the dispatch goes there.
Step 2: Test Resolution
curl -X POST -H "Authorization: Bearer $CHAINSAW_TOKEN" \
https://chain305.com/chainproxy/api/admin/ownership/resolve \
-d '{
"repo": "frontend-app",
"manifest_path": "package.json"
}'
Response:
{
"owner": "@frontend-team",
"destinations": {
"slack": "#frontend-secops",
"email": ["frontend-leads@example.com"]
},
"source": "github_codeowners",
"matched_pattern": "package.json"
}
If source: "default_fallback", the resolver couldn’t find an owner and is using the fallback row. Add an entry to CODEOWNERS or to the map.
Step 3: Write a Notification Policy
Open Policies → Create Policy:
| Setting | Value |
|---|---|
| Name | Notify owner: medium-severity vulnerabilities |
| Action | ActionNotifyOwner |
| Conditions | vulnerability.cvss >= 6 AND vulnerability.cvss < 8 |
| Surface | proxy |
| Scope | All repositories |
| Precedence | Below blocks, above passive monitoring |
When this fires, the manifest path of the requesting client is the input to the resolver. The owner’s destinations get the notification — Slack, email, or whatever the team’s row says.
The notification payload includes:
| Field | Example |
|---|---|
| Package | lodash@4.17.20 |
| Manifest | frontend-app/package.json |
| Owner resolved to | @frontend-team |
| Findings | vulnerability.cvss=7.4 (CVE-2026-2345) |
| Recommended action | “Upgrade to 4.17.22 — see [patch simulator]” |
| Deep links | violation detail, exception form, simulator |
Step 4: Read the Notification
A typical Slack notification looks like:
🟡 Chainsaw notified your team
Package: lodash@4.17.20 (npm)
Manifest: frontend-app/package.json
Severity: medium (CVSS 7.4)
Why: CVE-2026-2345
The install was NOT blocked. Recommended next steps:
• Upgrade lodash to 4.17.22 to clear this CVE
• [Investigate violation] [Create exception] [Simulate patch]
The deep links go to the dashboard pre-filled with the right context. The team can act in two clicks.
Step 5: Combine with Other Actions
ActionNotifyOwner composes with Block and Quarantine — fire a notification in addition to the action. Useful patterns:
Notify + block
For situations where you want to break the build but also tell the right team:
| Setting | Value |
|---|---|
| Action | Block |
| Also | ActionNotifyOwner |
| Conditions | vulnerability.kev_pinned == true |
KEV-pinned breaks the build; the owning team gets a Slack ping in the same instant — they don’t have to discover the breakage by looking at red CI.
Graduated rollout
The recommended pattern when introducing a new policy:
- Week 1:
ActionNotifyOwneronly — teams get notifications, builds keep working. - Week 2-3: still
ActionNotifyOwner— teams have time to upgrade voluntarily. - Week 4: switch to
Block— by now, every team has been notified for three weeks.
The toggle is one field on the policy. The graduated rollout means the eventual block-day is not a surprise.
Step 6: Audit the Dispatch
Every ActionNotifyOwner invocation is recorded in the audit log:
curl -H "Authorization: Bearer $CHAINSAW_TOKEN" \
"https://chain305.com/chainproxy/api/audit?event=action.notify_owner&since=7d" \
| jq '.events[] | {time, package, owner, destinations, success}'
The success field is true if every destination accepted the dispatch, false if any failed (failed dispatches retry per the webhook DLQ rules — see tutorial 59).
Step 7: Common Misconfigurations
| Symptom | Cause | Fix |
|---|---|---|
| Every notification goes to fallback | CODEOWNERS source is empty or unreadable | Confirm the file is at .github/CODEOWNERS, valid syntax, and that Chainsaw has read access to the repo metadata |
Notification body shows @team-name not the team’s destinations | Owner exists in CODEOWNERS but not in the destination map | Add a row to the destination map |
| Slack notification arrives in the wrong channel | Channel was renamed; webhook URL is stale | Mint a new incoming-webhook URL in Slack and update the destination |
| Per-team rate-limit spam | A single noisy policy is firing thousands of times | Tighten the policy condition; the rate-limiter on the destination caps outbound, but the right fix is fewer fires |
| Owner resolution slow | The CODEOWNERS file is huge and Chainsaw is fetching it on every fire | Enable the resolver cache (CHAINSAW_OWNERSHIP_CACHE_TTL=600s) |
Step 8: Per-Manifest Patterns Inside CODEOWNERS
CODEOWNERS supports glob patterns. Chainsaw’s resolver uses the standard CODEOWNERS resolution algorithm (last match wins):
# Default
* @platform-team
# All package.json
package.json @frontend-team
# Just the ML packages
src/ml/** @ml-team
A request for src/ml/requirements.txt resolves to @ml-team. A request for package.json resolves to @frontend-team. Anything else resolves to @platform-team.
This per-path resolution is the reason ActionNotifyOwner outperforms a single team-wide channel for everything. The right team gets the right ping for the right manifest.
Verification
- Configure CODEOWNERS for a test repo + add the team to the destination map.
- Run the
/api/admin/ownership/resolveendpoint — it returns the configured team. - Create a policy with
ActionNotifyOwnerand a condition matching a known package. - Trigger an install of that package — the team’s destinations receive the notification.
- Audit log shows the dispatch with
success: true.
Related Topics
- How to Send HMAC-Signed Outbound Webhooks — the destination plumbing this guide depends on.
- How to Manage Policy Precedence and Exceptions —
ActionNotifyOwnerlives in the same precedence model asBlockandAllow. - How to Use Billy MCP Tools for Triage — Billy’s
route_violation_to_ownertool is this same primitive, accessible from a chat.