Skip to content

About

Turns exploited and in-the-news Tenable findings into ServiceNow tickets grouped by fix, pages PagerDuty only for new urgency, and closes both when Tenable confirms the fix. A person approves every plan.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Exploit Dispatch for Tenable

Listed on CyberAgents Exchange

Exploit Dispatch turns the Tenable findings that need action now, the ones exploited in the wild or in the news, into ServiceNow tickets grouped by fix, and pages on-call through PagerDuty when one becomes newly urgent. A person approves every plan before anything is sent, and the tool resolves the tickets and pages when Tenable confirms the fix.

It runs as a Claude Code skill or as a standalone command-line tool.

Why

  • On-call hears about new exploitation, not the backlog. When an exploited vulnerability newly appears on a critical or internet-facing asset, it pages PagerDuty, with a link to the ticket. Older exploited findings get tickets but don't page.
  • Ticket floods hide what matters. One ticket per finding quickly becomes thousands. This tool opens one ticket per fix and owner, listing every affected asset, and only for exploited or in-the-news findings.
  • Tickets that never close. It resolves a ticket only when Tenable reports every asset fixed, and flags (rather than silently closes) tickets whose assets aged out or whose findings came back.
  • No ServiceNow Store app or extra license. It uses the standard ServiceNow Table API, works with Tenable Vulnerability Management (with or without Tenable One) and Tenable Security Center, and needs no Vulnerability Response module.

It's for the short list a team must act on now. For full synchronization of every finding, use Tenable's and ServiceNow's official integrations.

How it works

flowchart LR
    T[(Tenable)] --> C[collect]
    C --> P[plan]
    P --> A{You approve}
    A --> X["apply --confirm"]
    X --> SN[ServiceNow tickets]
    X --> PD[PagerDuty pages]
    SN -. owners patch, Tenable rescans .-> R[collect, then reconcile]
    R --> A2{You approve}
    A2 --> X2["apply --confirm"]
    X2 --> D[Tickets and pages resolved]
Loading
  1. collect runs one Tenable Vulnerability Management export, narrowed on the server to exploitation and news categories, and keeps findings whose plugins are flagged exploited (CISA KEV or exploited by malware) or in the news. Fixed findings are kept too, for reconcile. Each affected asset is looked up for its tags and criticality (ACR). Plugin output is never requested or stored. With --platform sc it reads Tenable Security Center instead; see Security Center.
  2. plan groups the findings into one ticket per fix and owner, checks ServiceNow for tickets it already opened, and writes a plan: tickets to create or update, pages to send, and anything deferred or needing attention. It changes nothing.
  3. apply <plan> checks that the plan is unchanged, recent, made with the same policy, and not applied before, and that every credential, routing key, and assignment group exists. With --confirm it sends exactly that plan. Just before sending, it checks ServiceNow again, so a ticket opened since the plan was made isn't duplicated.
  4. reconcile compares the tickets it opened with the latest findings: it resolves incidents (and their pages) whose assets are all fixed, adds a progress note when more are fixed, and flags tickets for a person when assets are missing or a closed ticket's assets are still affected.

What gets sent

  • ServiceNow (incident, or a change request for grouped patches if enabled): short description, a description with the reason, fix, vulnerabilities, CVEs, and affected assets, impact and urgency from severity, the assignment group from your routes, and a stable correlation_id used to find the ticket again. Updates and progress are work notes.
  • PagerDuty (Events API v2): a summary, severity, the asset list, CVEs and plugins, a link to the ServiceNow ticket, and a stable deduplication key, so repeats never open a second incident.

Paging

By default a group pages only when an exploited finding is on a critical (ACR 9 or higher) or internet-facing asset and is new: first found, or added to CISA KEV, in the last 7 days. Older exploited findings get tickets but no page, so pointing the tool at an existing backlog doesn't page on-call for it. In-the-news findings get tickets; paging for them is opt-in. All of this is set in the policy.

What it looks like

These are from the demo, run against a ServiceNow developer instance and a PagerDuty developer account. The hosts are made up; the vulnerabilities are real.

Claude Code makes the plan, explains it, and waits for approval before sending anything:

Claude Code explaining a plan of three tickets and one page, and asking for approval

One incident per fix and owner, listing the reason, the fix, the CVEs, and every affected asset:

A ServiceNow incident for the PHP fix, assigned to Application Development, listing web01 and web02

A page only for the new, urgent part, linked to its ticket:

A PagerDuty incident with the reasons it paged, the affected asset, and a link to the ServiceNow incident

The demo's README walks through every step with screenshots, including the work notes and the resolve after patching.

Installation

As a Claude Code skill

git clone https://github.com/lanz/exploit-dispatch ~/.claude/skills/exploit-dispatch

Then ask, for example, "Ticket the exploited vulnerabilities on our web servers", or run /exploit-dispatch. The skill always shows you the plan and waits for your approval before sending anything.

Standalone

git clone https://github.com/lanz/exploit-dispatch
cd exploit-dispatch
uv run scripts/dispatch.py --help      # uv installs the dependencies from the script's metadata

Python 3.11 or later. Without uv: pip install "pytenable>=26.10.1" requests pydantic truststore.

Configuration

The policy file

Copy examples/policy.example.toml and edit it. It sets:

  • which signals open a ticket, and the minimum severity
  • grouping (fix, or one ticket per plugin), change requests for grouped patches, and the maximum tickets per run
  • paging rules and the maximum pages per run
  • the ServiceNow instance, the resolve state and close code reconcile uses, and an optional caller for new incidents
  • routes: a Tenable tag selects the ServiceNow assignment group and PagerDuty service
  • impact and urgency by severity

dispatch.py policy <file> checks the file and explains what it will do.

Credentials

Credentials come only from environment variables, never from files or the command line.

Variable Used for
TIO_ACCESS_KEY, TIO_SECRET_KEY, optionally TIO_URL Tenable Vulnerability Management; another prefix with --env-prefix
TSC_URL, TSC_ACCESS_KEY, TSC_SECRET_KEY Tenable Security Center (--platform sc); another prefix with --env-prefix
EXD_SERVICENOW_USER and EXD_SERVICENOW_PASSWORD, or EXD_SERVICENOW_TOKEN (OAuth) ServiceNow
EXD_PAGERDUTY_KEY_<SERVICE> One Events API v2 integration key per PagerDuty service named in the policy
EXD_SYSTEM_CERTS=1 Trust the operating system's certificates, behind a TLS-inspecting proxy
EXD_HOME Where plans, findings, the ledger, and the audit log go (default ~/.cache/exploit-dispatch)

Tenable: in Vulnerability Management, the API user needs to export vulnerabilities and read the assets in scope. In Security Center, use an organization user (security manager or analyst) with access to the repositories in scope; administrators see no vulnerability data.

ServiceNow: use a dedicated integration user with the itil role. That is enough for everything the tool does. It reads incidents and change requests (through the task table), assignment groups, and the incident close codes. It creates incidents and change requests, adds work notes, and resolves incidents. This was verified on a developer instance with a user that had only itil (and the roles it includes). Users without access to sys_choice get the close codes from the incident table's UI metadata instead. Consider setting the user to "Web service access only".

PagerDuty: for each service, add an Events API V2 integration and use its integration key. No PagerDuty app or OAuth is needed. Turning off the service's alert grouping keeps one incident per fix group.

Usage

R=scripts/dispatch.py

uv run $R policy my-policy.toml
uv run $R collect --policy my-policy.toml                    # prints the findings file
uv run $R plan --findings <findings file> --policy my-policy.toml
uv run $R apply <plan file> --policy my-policy.toml          # checks only
uv run $R apply <plan file> --policy my-policy.toml --confirm

# after the fixes are rescanned
uv run $R collect --policy my-policy.toml
uv run $R reconcile --findings <findings file> --policy my-policy.toml
uv run $R apply <reconcile plan file> --policy my-policy.toml --confirm

To see the whole flow end to end against a ServiceNow developer instance and a PagerDuty developer account, follow examples/demo: three days of findings for real vulnerabilities on made-up hosts.

Useful options:

  • collect --tag CATEGORY:VALUE and --plugin-id ID narrow the export; --since-days (default 90) sets how far back findings, and fixes, are read.
  • plan --offline plans without checking ServiceNow (apply still checks before creating).
  • apply --max-age-hours (default 24) sets how old a plan may be.

Security Center

collect --platform sc reads Security Center's cumulative results (open findings, marked reopened when previously mitigated) and patched results (fixed findings), narrowed on the server to CISA KEV findings and the policy's minimum severity. Two differences from Vulnerability Management:

  • Only the exploited (CISA KEV) signal. Security Center records CISA KEV in each finding's cross-references, with the date it was added, but has no in-the-news or exploited-by-malware data.

  • Routing by repository. Security Center has no asset tags, so each finding gets the tag Repository:<name>, and routes match it like any other tag:

    [[routes]]
    name = "dmz"
    match_tags = [{ category = "Repository", value = "Web DMZ" }]
    assignment_group = "Web Platform Operations"

Asset criticality (ACR) comes from each finding, so criticality-based paging works the same way. Certificates are verified; for a private CA set REQUESTS_CA_BUNDLE or EXD_SYSTEM_CERTS=1, and use --insecure only for a lab.

Playbook: with Tenable One

With Tenable One, the Hexa AI MCP, and these CyberAgents Exchange skills, the skill can run as the last step of a playbook:

  1. Prioritize with the Remediation Priority & Impact Agent (/fix-today) or, for one CVE, the CVE Impact Analyzer.
  2. Escalate what you choose with this tool: collect --plugin-id …, then plan, approve, and apply.
  3. Close the loop with reconcile after the fixes are rescanned.

The findings always come from Tenable through collect, never from another tool's text.

Data handling and safety

  • Nothing is sent without approval. Only apply --confirm writes, and only the plan you approved. A plan whose contents no longer match its hash, such as one edited by mistake, is refused.
  • No duplicates, no deletes. Tickets are found by correlation ID before every create; pages use stable deduplication keys. The tool never deletes anything.
  • Plugin output never leaves Tenable. It isn't requested, stored, or sent.
  • Secrets stay in the environment. Routing keys and passwords never appear in plans, logs, or errors.
  • Local files are owner-only. Plans, findings, the ledger (which tickets and pages the tool opened), and the append-only audit log are written with owner-only permissions under EXD_HOME. They list host names; treat them as internal.
  • Network access: your Tenable URL, your ServiceNow instance, and events.pagerduty.com. No telemetry.

Known limitations

  • Security Center findings carry only the CISA KEV signal, and route by repository rather than tags (see Security Center).
  • The ledger is local. It remembers each ticket's assets and the open pages. Run the tool from one place, or share EXD_HOME, so runs see the same ledger.
  • Change requests are never resolved automatically; they get a note and a flag.
  • Pages resolved by hand in PagerDuty still count as open in the ledger until reconcile resolves them.
  • Paging needs dates. A finding without a first-found date or KEV date can't be shown to be new, so it doesn't page.
  • The age limit is for the plan, not the findings. apply refuses a plan more than 24 hours old, but a plan made today from an old findings file passes. Collect fresh findings before planning.
  • Assets past --max-asset-lookups (default 1000), or whose lookup fails, have no tags. When the policy has routes, their findings are left out, so they don't go to the wrong team; collect reports how many. Narrow the collection with --tag, or raise the limit.

Development

pip install "pytenable>=26.10.1" requests pydantic truststore pytest responses ruff
pytest
ruff check scripts tests

The tests use synthetic data and mocked HTTP; they don't contact Tenable, ServiceNow, or PagerDuty.

License

Licensed under the MIT License. Copyright (c) 2026 Lance Duvall.

Disclaimer

This is an independent open source community project. It is not an official Tenable product and is not affiliated with, sponsored by, endorsed by, or supported by Tenable, Inc.

Tenable, Nessus, and other Tenable product names are trademarks or registered trademarks of Tenable, Inc. ServiceNow and PagerDuty are trademarks of their respective owners. Other names may be trademarks of their respective owners.

About

Turns exploited and in-the-news Tenable findings into ServiceNow tickets grouped by fix, pages PagerDuty only for new urgency, and closes both when Tenable confirms the fix. A person approves every plan.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages