How to Route Violations to CODEOWNERS-Resolved Owners with ActionNotifyOwner

Intermediate 25 minutes Security Engineers / Engineering Managers Security & Policy

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

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:

  1. The repository’s .github/CODEOWNERS (or CODEOWNERS at the root, or docs/CODEOWNERS).
  2. The org-level fallback configured here.
  3. The default @security-team if everything else is empty.

Most setups just point at the repo and rely on per-repo CODEOWNERS files.

Destination map

OwnerSlack channelPagerDutyEmail
@frontend-team#frontend-secops—frontend-leads@example.com
@platform-team#platform-secopsPD-Schedule-Platform—
@ml-team#ml-supply-chain—ml-leads@example.com
@security-team (fallback)#securityPD-Schedule-Securitysecops@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:

SettingValue
NameNotify owner: medium-severity vulnerabilities
ActionActionNotifyOwner
Conditionsvulnerability.cvss >= 6 AND vulnerability.cvss < 8
Surfaceproxy
ScopeAll repositories
PrecedenceBelow 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:

FieldExample
Packagelodash@4.17.20
Manifestfrontend-app/package.json
Owner resolved to@frontend-team
Findingsvulnerability.cvss=7.4 (CVE-2026-2345)
Recommended action“Upgrade to 4.17.22 — see [patch simulator]”
Deep linksviolation 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:

SettingValue
ActionBlock
AlsoActionNotifyOwner
Conditionsvulnerability.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:

  1. Week 1: ActionNotifyOwner only — teams get notifications, builds keep working.
  2. Week 2-3: still ActionNotifyOwner — teams have time to upgrade voluntarily.
  3. 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

SymptomCauseFix
Every notification goes to fallbackCODEOWNERS source is empty or unreadableConfirm 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 destinationsOwner exists in CODEOWNERS but not in the destination mapAdd a row to the destination map
Slack notification arrives in the wrong channelChannel was renamed; webhook URL is staleMint a new incoming-webhook URL in Slack and update the destination
Per-team rate-limit spamA single noisy policy is firing thousands of timesTighten the policy condition; the rate-limiter on the destination caps outbound, but the right fix is fewer fires
Owner resolution slowThe CODEOWNERS file is huge and Chainsaw is fetching it on every fireEnable 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

  1. Configure CODEOWNERS for a test repo + add the team to the destination map.
  2. Run the /api/admin/ownership/resolve endpoint — it returns the configured team.
  3. Create a policy with ActionNotifyOwner and a condition matching a known package.
  4. Trigger an install of that package — the team’s destinations receive the notification.
  5. Audit log shows the dispatch with success: true.