Add 4 audit prompts: phone_inventory, user_audit, inbound_did_audit, hunt_pilot_audit
Builds on the prompts-package extraction. Each new prompt embeds
schema-verified SQL plus a findings template tuned to surface
audit-actionable issues (orphans, drift, capacity outliers, security
posture).
phone_inventory_report(filter=None):
Aggregates by model / device pool / CSS, then anomaly queries for
phones with no description, phones whose description echoes their
MAC-based name, phones with no owner, phones in non-default CSS.
Cross-references owner status (phones owned by inactive users
surface as findings).
user_audit(focus=full|admin|inactive|app_users):
End user + application user inventory, role/group assignments via
the enduserdirgroupmap → dirgroup → functionroledirgroupmap →
functionrole join chain. Security-critical findings: app users
with admin-grade role memberships, local-user accounts with admin
privileges, phones owned by inactive users.
inbound_did_audit():
Reusable form of today's cucm-inbound-did-inventory work. XFORM-
Inbound-DNIS curated list categorized (pass-through, block-trans,
specific renames, wildcards, catch-all hazard). Cross-checked
against Internal-PT route patterns and the operator-curated
PSTN-Screen-PT spam blocklist. Findings for orphan target
extensions and the silent !-catch-all risk.
hunt_pilot_audit():
Hunt pilot inventory with queue settings, line group membership,
and distribution algorithm decoding. Schema knowledge already
Hamilton-verified: huntpilotqueue joins via fknumplan_pilot, NOT
fknumplan (the test asserts the correct column appears in the
rendered prompt). Findings: queue misconfigurations (NULL
destinations, infinite max-wait), empty line groups, dead pilots
with no route-list destination.
Implementation notes:
- Each prompt's SQL was validated against the live cluster
(cucm-pub.binghammemorial.org, CUCM 15.0.1.12900-234).
- user_audit originally used UNION ALL with NULL-typed status
column for the headcounts query; Informix rejected it. Split
into two simpler queries (commented in the prompt body).
- phone_inventory_report uses a Hamilton-style SQL escape for
the optional name_filter (single quotes doubled).
- All four prompts gracefully degrade when the docs index isn't
loaded (verified by test_all_new_prompts_render_without_docs).
114 → 123 tests; 5 → 9 prompts. Full live-cluster verification:
- 12 phone models, 629 Cisco 7841 phones (largest model)
- 1,246 active end users, 25 application users
- Hunt pilots with named distribution algorithms (Broadcast, Top
Down, etc.) — confirms typedistributealgorithm join works
- Hamilton-fixed huntpilotqueue.fknumplan_pilot column verified
in the embedded SQL via dedicated regression test.
This commit is contained in:
parent
e6aa075793
commit
8aaeb04417
@ -20,15 +20,23 @@ shim is where the parameter contract lives.
|
|||||||
from . import (
|
from . import (
|
||||||
audit_routing,
|
audit_routing,
|
||||||
cucm_sql_help,
|
cucm_sql_help,
|
||||||
|
hunt_pilot_audit,
|
||||||
|
inbound_did_audit,
|
||||||
investigate_pattern,
|
investigate_pattern,
|
||||||
|
phone_inventory_report,
|
||||||
route_plan_overview,
|
route_plan_overview,
|
||||||
sip_trunk_report,
|
sip_trunk_report,
|
||||||
|
user_audit,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"audit_routing",
|
"audit_routing",
|
||||||
"cucm_sql_help",
|
"cucm_sql_help",
|
||||||
|
"hunt_pilot_audit",
|
||||||
|
"inbound_did_audit",
|
||||||
"investigate_pattern",
|
"investigate_pattern",
|
||||||
|
"phone_inventory_report",
|
||||||
"route_plan_overview",
|
"route_plan_overview",
|
||||||
"sip_trunk_report",
|
"sip_trunk_report",
|
||||||
|
"user_audit",
|
||||||
]
|
]
|
||||||
|
|||||||
207
src/mcp_cucm_axl/prompts/hunt_pilot_audit.py
Normal file
207
src/mcp_cucm_axl/prompts/hunt_pilot_audit.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
"""Hunt pilot + line group + queue settings audit."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from ._common import render_schema_block
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..docs_loader import DocsIndex
|
||||||
|
|
||||||
|
|
||||||
|
_KEYWORDS = [
|
||||||
|
"hunt pilot", "hunt list", "line group", "distribution algorithm",
|
||||||
|
"queue", "rna", "ring no answer", "huntpilotqueue",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def render(docs: "DocsIndex | None") -> str:
|
||||||
|
"""Hunt pilot inventory and audit. Schema-aware (uses
|
||||||
|
`huntpilotqueue.fknumplan_pilot`, NOT `fknumplan` — verified against
|
||||||
|
CUCM 15 schema 2026-04-25)."""
|
||||||
|
schema_block = render_schema_block(
|
||||||
|
docs, _KEYWORDS, max_chunks=4, max_chars_per_chunk=900
|
||||||
|
)
|
||||||
|
|
||||||
|
return """# CUCM Hunt Pilot Audit
|
||||||
|
|
||||||
|
Hunt pilots distribute incoming calls across a group of phones (hunt
|
||||||
|
group) with configurable algorithms (top-down, longest-idle, broadcast,
|
||||||
|
etc.) and queue behavior. Misconfigured hunt pilots are a common source
|
||||||
|
of "calls disappear into the void" complaints.
|
||||||
|
|
||||||
|
## Schema (CUCM 15)
|
||||||
|
|
||||||
|
- `numplan` row with `tkpatternusage = 7` is a Hunt Pilot
|
||||||
|
- `huntpilotqueue` joins to numplan via `fknumplan_pilot` (NOT
|
||||||
|
`fknumplan` — that's a common-mistake column name)
|
||||||
|
- `linegroup` defines the distribution algorithm and member-handling
|
||||||
|
- `linegroupnumplanmap` joins line groups to their member DNs
|
||||||
|
- A hunt pilot points to a "Route List" device (just like route
|
||||||
|
patterns do) — that route list contains route groups containing
|
||||||
|
line groups containing phones
|
||||||
|
|
||||||
|
## Step 1 — Hunt pilot inventory
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
np.dnorpattern AS hunt_pilot,
|
||||||
|
rp.name AS partition,
|
||||||
|
np.description,
|
||||||
|
np.calledpartytransformationmask AS xform_called,
|
||||||
|
np.callingpartytransformationmask AS xform_calling,
|
||||||
|
np.pkid
|
||||||
|
FROM numplan np
|
||||||
|
JOIN routepartition rp ON np.fkroutepartition = rp.pkid
|
||||||
|
WHERE np.tkpatternusage = 7 -- Hunt Pilot
|
||||||
|
ORDER BY rp.name, np.dnorpattern;
|
||||||
|
```
|
||||||
|
|
||||||
|
Each hunt pilot is a "front door" pattern — what callers dial to reach
|
||||||
|
a group. The transformations columns matter: a hunt pilot may rewrite
|
||||||
|
the called party (e.g., normalize to a base-extension) before
|
||||||
|
distribution.
|
||||||
|
|
||||||
|
## Step 2 — Queue settings per hunt pilot
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
np.dnorpattern AS hunt_pilot,
|
||||||
|
hpq.maxcallersinqueue,
|
||||||
|
hpq.maxwaittimeinqueue,
|
||||||
|
hpq.maxwaittimedestination AS overflow_dest,
|
||||||
|
hpq.noagentdestination AS no_agent_dest,
|
||||||
|
hpq.queuefulldestination AS queue_full_dest,
|
||||||
|
css_max.name AS css_for_max_wait,
|
||||||
|
css_no.name AS css_for_no_agent,
|
||||||
|
css_full.name AS css_for_queue_full
|
||||||
|
FROM huntpilotqueue hpq
|
||||||
|
JOIN numplan np ON hpq.fknumplan_pilot = np.pkid
|
||||||
|
LEFT OUTER JOIN callingsearchspace css_max ON hpq.fkcallingsearchspace_maxwaittime = css_max.pkid
|
||||||
|
LEFT OUTER JOIN callingsearchspace css_no ON hpq.fkcallingsearchspace_noagent = css_no.pkid
|
||||||
|
LEFT OUTER JOIN callingsearchspace css_full ON hpq.fkcallingsearchspace_pilotqueuefull = css_full.pkid
|
||||||
|
ORDER BY np.dnorpattern;
|
||||||
|
```
|
||||||
|
|
||||||
|
Queue behavior is the most-misconfigured aspect of hunt pilots:
|
||||||
|
- `maxwaittimeinqueue = 0` means "no max" — callers can wait forever.
|
||||||
|
Usually a misconfiguration; should be set to a sensible value
|
||||||
|
(e.g., 30-300 seconds) with an overflow destination.
|
||||||
|
- `maxwaittimedestination` / `noagentdestination` /
|
||||||
|
`queuefulldestination` define what happens when each condition
|
||||||
|
triggers. NULL on any of these means "drop the call" — almost never
|
||||||
|
the operator's intent.
|
||||||
|
|
||||||
|
## Step 3 — Line groups and their member DNs
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
lg.name AS line_group,
|
||||||
|
ta.name AS distribution_algorithm,
|
||||||
|
np.dnorpattern AS member_dn,
|
||||||
|
rp.name AS member_partition,
|
||||||
|
lgnpm.lineselectionorder AS sortorder
|
||||||
|
FROM linegroup lg
|
||||||
|
LEFT OUTER JOIN typedistributealgorithm ta ON lg.tkdistributealgorithm = ta.enum
|
||||||
|
LEFT OUTER JOIN linegroupnumplanmap lgnpm ON lgnpm.fklinegroup = lg.pkid
|
||||||
|
LEFT OUTER JOIN numplan np ON lgnpm.fknumplan = np.pkid
|
||||||
|
LEFT OUTER JOIN routepartition rp ON np.fkroutepartition = rp.pkid
|
||||||
|
ORDER BY lg.name, lgnpm.lineselectionorder;
|
||||||
|
```
|
||||||
|
|
||||||
|
Distribution algorithms (decoded via `typedistributealgorithm`):
|
||||||
|
- **Top Down** — first member always rings first; predictable but
|
||||||
|
uneven load.
|
||||||
|
- **Circular** — round-robin starting after the last-rung member.
|
||||||
|
- **Longest Idle Time** — call goes to the member who hasn't rung in
|
||||||
|
the longest time. Most common for fairness.
|
||||||
|
- **Broadcast** — every member rings simultaneously. Use sparingly;
|
||||||
|
noisy.
|
||||||
|
|
||||||
|
## Step 4 — Hunt pilot → route list / line group destinations
|
||||||
|
|
||||||
|
The hunt pilot routes to a Route List that contains the actual line
|
||||||
|
group(s). Use the existing tool:
|
||||||
|
|
||||||
|
```
|
||||||
|
route_inspect_pattern(<hunt_pilot_dn>, <partition>)
|
||||||
|
```
|
||||||
|
|
||||||
|
This returns the destination chain. For each hunt pilot identified in
|
||||||
|
Step 1, run this to confirm:
|
||||||
|
- Destination is the expected route list / line group
|
||||||
|
- The line group's distribution algorithm matches operational intent
|
||||||
|
- Member DNs all exist and belong to active phones (cross-reference
|
||||||
|
with `phone_inventory_report`)
|
||||||
|
|
||||||
|
## Step 5 — Hunt pilots with no line group destination (dead pilots)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Hunt pilots that don't appear in any line group routing
|
||||||
|
SELECT np.dnorpattern, rp.name AS partition, np.description
|
||||||
|
FROM numplan np
|
||||||
|
JOIN routepartition rp ON np.fkroutepartition = rp.pkid
|
||||||
|
WHERE np.tkpatternusage = 7
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM devicenumplanmap dnm
|
||||||
|
JOIN device d ON dnm.fkdevice = d.pkid
|
||||||
|
JOIN typeclass tc ON d.tkclass = tc.enum
|
||||||
|
WHERE dnm.fknumplan = np.pkid AND tc.name = 'Route List'
|
||||||
|
)
|
||||||
|
ORDER BY rp.name, np.dnorpattern;
|
||||||
|
```
|
||||||
|
|
||||||
|
A hunt pilot that doesn't route to a route list is functionally dead —
|
||||||
|
calls match the pilot pattern but have nowhere to go. Often vestigial
|
||||||
|
config.
|
||||||
|
|
||||||
|
## Findings to call out
|
||||||
|
|
||||||
|
### Queue misconfigurations
|
||||||
|
- **`maxwaittimeinqueue = 0`** without an explicit overflow rationale
|
||||||
|
— callers can wait forever in queue.
|
||||||
|
- **NULL `maxwaittimedestination` / `noagentdestination` /
|
||||||
|
`queuefulldestination`** — calls drop without going anywhere.
|
||||||
|
Recommend explicit destinations (typically voicemail).
|
||||||
|
- **Mismatched CSSs** on the three queue destinations: the CSSs control
|
||||||
|
whether the destination is reachable. If the CSS is overly restrictive,
|
||||||
|
the overflow destination might not be reachable from the hunt pilot's
|
||||||
|
context.
|
||||||
|
|
||||||
|
### Line group hygiene
|
||||||
|
- **Empty line groups** (no members in `linegroupnumplanmap`) — calls
|
||||||
|
would never ring anywhere.
|
||||||
|
- **Line groups with one member** — fine, but check whether a hunt
|
||||||
|
pilot is overkill (a direct DN may be simpler).
|
||||||
|
- **Members in an inactive partition** or pointing to a DN that's
|
||||||
|
no longer assigned to any phone.
|
||||||
|
|
||||||
|
### Distribution algorithm clarity
|
||||||
|
- **Distribution = Broadcast** for a large group — operationally noisy;
|
||||||
|
confirm with operator whether this is intentional.
|
||||||
|
- **Distribution = Top Down** with the same first member every time —
|
||||||
|
load concentrates on one phone; operator may want longest-idle.
|
||||||
|
|
||||||
|
### Coverage gaps
|
||||||
|
- **Hunt pilots not pointing to a route list** (Step 5) — cleanup.
|
||||||
|
- **Hunt pilot description missing** — operationally opaque; recommend
|
||||||
|
description annotations.
|
||||||
|
|
||||||
|
## Suggested follow-up calls
|
||||||
|
|
||||||
|
- `route_inspect_pattern(<pilot>, <partition>)` for each hunt pilot to
|
||||||
|
trace its destination chain.
|
||||||
|
- `route_devices_using_css(<each unique queue-destination CSS>)` to
|
||||||
|
understand the blast radius of the CSSs the queue uses.
|
||||||
|
- `axl_describe_table('huntpilotqueue')` if the audit needs additional
|
||||||
|
queue-config columns (announcement, MoH source, etc.).
|
||||||
|
|
||||||
|
## Reference: CUCM data dictionary (hunt pilots, line groups)
|
||||||
|
|
||||||
|
""" + schema_block + """
|
||||||
|
|
||||||
|
Run the queries above and produce a structured findings report. Group
|
||||||
|
by hunt pilot; under each, list its queue config, line group(s), and
|
||||||
|
distribution algorithm; then list the audit findings with severity.
|
||||||
|
"""
|
||||||
181
src/mcp_cucm_axl/prompts/inbound_did_audit.py
Normal file
181
src/mcp_cucm_axl/prompts/inbound_did_audit.py
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
"""Inbound DID inventory: XFORM-Inbound-DNIS, screening, executed routing."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from ._common import render_schema_block
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..docs_loader import DocsIndex
|
||||||
|
|
||||||
|
|
||||||
|
_KEYWORDS = [
|
||||||
|
"translation pattern", "called party transformation", "DNIS",
|
||||||
|
"inbound", "route pattern", "transformation mask", "PSTN",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def render(docs: "DocsIndex | None") -> str:
|
||||||
|
"""Inventory of every PSTN DID that *might* be presented to the cluster,
|
||||||
|
cross-referenced against actual routing destinations.
|
||||||
|
|
||||||
|
Pattern: turn the operator-curated XFORM-Inbound-DNIS list into a
|
||||||
|
"presentable DID inventory" with destination cross-checks. Surfaces
|
||||||
|
orphan target extensions, undocumented DIDs, and the silent-fallback
|
||||||
|
catch-all hazard.
|
||||||
|
"""
|
||||||
|
schema_block = render_schema_block(
|
||||||
|
docs, _KEYWORDS, max_chunks=4, max_chars_per_chunk=900
|
||||||
|
)
|
||||||
|
|
||||||
|
return """# CUCM Inbound DID Audit
|
||||||
|
|
||||||
|
The cluster's inbound architecture has multiple transformation layers.
|
||||||
|
This audit reconstructs the full set of DIDs that "should" be presentable
|
||||||
|
and cross-checks them against actual routing destinations.
|
||||||
|
|
||||||
|
## Conceptual model (verify on this cluster)
|
||||||
|
|
||||||
|
Inbound calls usually traverse:
|
||||||
|
|
||||||
|
1. PSTN-facing trunk (typically `PSTN-Router-SIP-Trk` or similar) →
|
||||||
|
trunk's primary CSS, e.g. `PSTN-Inbound-CSS`.
|
||||||
|
2. `PSTN-Inbound-PT` partition with a `!` translation that re-routes to
|
||||||
|
`PSTN-Screen-CSS` for spam filtering.
|
||||||
|
3. `PSTN-Screen-PT` with operator-curated `block_enabled=true` translation
|
||||||
|
patterns for spam blocking, plus a `!` / `\\+!` catch-all that
|
||||||
|
re-routes via `Internal-CSS`.
|
||||||
|
4. `Internal-CSS` finds the destination — either a 10-digit route pattern
|
||||||
|
in `Internal-PT` (typically routes to fax trunks) or a directory
|
||||||
|
number after some upstream transformation.
|
||||||
|
|
||||||
|
The XFORM-Inbound-DNIS partition holds 100+ "Called Party Number
|
||||||
|
Transformation" patterns (`tkpatternusage = 20`) that map external DIDs
|
||||||
|
to internal extensions. **Whether they fire automatically depends on
|
||||||
|
the trunk's `fkcallingsearchspace_cntdpntransform` setting** — confirm
|
||||||
|
this with the operator or via:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT d.name, sd.fkcallingsearchspace_cntdpntransform
|
||||||
|
FROM device d JOIN sipdevice sd ON sd.fkdevice = d.pkid
|
||||||
|
JOIN typeclass tc ON d.tkclass = tc.enum
|
||||||
|
WHERE tc.name = 'Trunk';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1 — Operator-curated DID inventory (XFORM-Inbound-DNIS)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
np.dnorpattern AS inbound_did,
|
||||||
|
np.calledpartytransformationmask AS xform_to,
|
||||||
|
np.prefixdigitsout AS prefix_out,
|
||||||
|
np.description
|
||||||
|
FROM numplan np
|
||||||
|
JOIN routepartition rp ON np.fkroutepartition = rp.pkid
|
||||||
|
WHERE rp.name = 'XFORM-Inbound-DNIS'
|
||||||
|
ORDER BY np.dnorpattern;
|
||||||
|
```
|
||||||
|
|
||||||
|
Categorize the result:
|
||||||
|
|
||||||
|
- **Pass-through 10-digit** (`xform_to IS NULL`, description "remain as 10-digit"):
|
||||||
|
the DID is expected to match a corresponding route pattern in `Internal-PT`.
|
||||||
|
- **Block-translation to 4-digit** (`xform_to LIKE '7XXX'` or similar):
|
||||||
|
rewrites last N digits to an internal extension range.
|
||||||
|
- **Specific renames** (literal `xform_to` like `7400`, `2523`):
|
||||||
|
one-off DID-to-extension mappings, often legacy clinic numbers.
|
||||||
|
- **Wildcard ranges** (pattern contains `[N-M]`, `XXX`, `.`):
|
||||||
|
efficient block mappings worth reviewing for bounds correctness.
|
||||||
|
- **Catch-all `!`**: dangerous silent fallback if present — flag it.
|
||||||
|
|
||||||
|
## Step 2 — Actually-executed routing (Internal-PT route patterns ≥7 digits)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT np.dnorpattern, np.description, np.blockenable
|
||||||
|
FROM numplan np
|
||||||
|
JOIN routepartition rp ON np.fkroutepartition = rp.pkid
|
||||||
|
WHERE rp.name = 'Internal-PT'
|
||||||
|
AND np.tkpatternusage = 5 -- Route pattern
|
||||||
|
AND LENGTH(np.dnorpattern) >= 7
|
||||||
|
ORDER BY np.dnorpattern;
|
||||||
|
```
|
||||||
|
|
||||||
|
Cross-check:
|
||||||
|
- Every "remain as 10-digit" DID from Step 1 should have a matching
|
||||||
|
10-digit route pattern here.
|
||||||
|
- Every route pattern here should have a known purpose (typically routes
|
||||||
|
to a fax trunk or hunt list).
|
||||||
|
|
||||||
|
## Step 3 — Spam blocklist (operator-curated, audit-relevant)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT np.dnorpattern, np.description
|
||||||
|
FROM numplan np
|
||||||
|
JOIN routepartition rp ON np.fkroutepartition = rp.pkid
|
||||||
|
WHERE rp.name = 'PSTN-Screen-PT' AND np.blockenable = 't'
|
||||||
|
ORDER BY np.dnorpattern;
|
||||||
|
```
|
||||||
|
|
||||||
|
Spam blocklists rotate every 30-60 days as campaigns shift. This is
|
||||||
|
informational, but persistently-stale entries (descriptions like
|
||||||
|
"Number spamming X's phone" from 1+ year ago) can be retired.
|
||||||
|
|
||||||
|
## Step 4 — Verify target extensions exist
|
||||||
|
|
||||||
|
For each unique target extension referenced in Step 1's renames, confirm
|
||||||
|
it actually exists as a real DN:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Replace '7400, 2523, 2532, ...' with the unique targets from Step 1
|
||||||
|
SELECT FIRST 50 np.dnorpattern, np.description, rp.name AS partition
|
||||||
|
FROM numplan np
|
||||||
|
JOIN routepartition rp ON np.fkroutepartition = rp.pkid
|
||||||
|
WHERE np.tkpatternusage = 2 -- Directory Number
|
||||||
|
AND np.dnorpattern IN ('7400', '2523', '2532', '...')
|
||||||
|
ORDER BY np.dnorpattern;
|
||||||
|
```
|
||||||
|
|
||||||
|
A DID that translates to an extension that doesn't exist is a *broken*
|
||||||
|
inbound DID — calls succeed in the transformation step but fail at the
|
||||||
|
final DN lookup.
|
||||||
|
|
||||||
|
## Findings to call out
|
||||||
|
|
||||||
|
- **Orphan target extensions**: any DID that translates to a non-existent
|
||||||
|
DN. High-severity audit finding — caller hears "number not found" or
|
||||||
|
reorder.
|
||||||
|
- **`!` catch-all in XFORM-Inbound-DNIS**: silently rewrites unrecognized
|
||||||
|
DIDs to their last 4 digits. Recommend documenting or removing — at
|
||||||
|
minimum, surface the risk.
|
||||||
|
- **Old clinic / legacy DIDs**: if multiple DIDs forward to the same
|
||||||
|
internal extension with descriptions referencing legacy locations,
|
||||||
|
confirm the target is a hunt pilot or reception line, not a single
|
||||||
|
user receiving misdirected calls.
|
||||||
|
- **Block-of-N pass-through declarations** (e.g., `20878594XX` declares
|
||||||
|
100 DIDs): verify carrier allocation matches the block size. Excess
|
||||||
|
declarations are harmless but noisy.
|
||||||
|
- **Spam blocklist hygiene**: > 6-month-old entries that are no longer
|
||||||
|
active campaigns are cleanup candidates.
|
||||||
|
- **Stale block_enabled outbound DIDs** (`Internal-PT` route patterns
|
||||||
|
with `blockenable = 't'`): confirm intentional. Single oddballs among
|
||||||
|
many similar route patterns warrant a description annotation.
|
||||||
|
|
||||||
|
## Suggested follow-up calls
|
||||||
|
|
||||||
|
- `route_inspect_pattern(<DID>)` for any specific DID where the audit
|
||||||
|
needs the full route trace (CSS reachability, destination chain).
|
||||||
|
- `axl_sql("SELECT * FROM numplan WHERE dnorpattern = '<target ext>'")`
|
||||||
|
to verify each rewrite target exists.
|
||||||
|
- `route_devices_using_css('PSTN-Inbound-CSS')` to confirm the inbound
|
||||||
|
trunk(s) using this CSS — should typically be just 1.
|
||||||
|
|
||||||
|
## Reference: CUCM data dictionary (translation patterns)
|
||||||
|
|
||||||
|
""" + schema_block + """
|
||||||
|
|
||||||
|
Run the queries above and produce a structured findings report. Group
|
||||||
|
by DID handling category (Step 1 categorization), then list orphan-
|
||||||
|
target findings and other hygiene issues. Don't enumerate all 100+
|
||||||
|
DIDs — group by exchange (208-XXX-) and call out exceptions.
|
||||||
|
"""
|
||||||
197
src/mcp_cucm_axl/prompts/phone_inventory_report.py
Normal file
197
src/mcp_cucm_axl/prompts/phone_inventory_report.py
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
"""Phone inventory + audit findings: device pools, CSSs, owners, anomalies."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from ._common import render_schema_block
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..docs_loader import DocsIndex
|
||||||
|
|
||||||
|
|
||||||
|
_KEYWORDS = [
|
||||||
|
"phone", "device", "device pool", "calling search space",
|
||||||
|
"model", "owner", "extension mobility",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _esc(s: str) -> str:
|
||||||
|
return s.replace("'", "''")
|
||||||
|
|
||||||
|
|
||||||
|
def render(docs: "DocsIndex | None", filter: str | None = None) -> str:
|
||||||
|
"""Phone inventory: counts by class, distribution by pool/CSS/model,
|
||||||
|
plus findings around orphans, drift, and naming anomalies.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filter: Optional substring (case-sensitive LIKE on `device.name`
|
||||||
|
OR `device.description`) to narrow the inventory.
|
||||||
|
"""
|
||||||
|
schema_block = render_schema_block(
|
||||||
|
docs, _KEYWORDS, max_chunks=4, max_chars_per_chunk=900
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter:
|
||||||
|
safe = _esc(filter)
|
||||||
|
name_clause = f"\n AND (d.name LIKE '%{safe}%' OR d.description LIKE '%{safe}%')"
|
||||||
|
scope_note = f"narrowed to phones matching `%{filter}%`"
|
||||||
|
else:
|
||||||
|
name_clause = ""
|
||||||
|
scope_note = "all phones"
|
||||||
|
|
||||||
|
return f"""# CUCM Phone Inventory — {scope_note}
|
||||||
|
|
||||||
|
Produce a phone inventory with audit-relevant groupings and findings.
|
||||||
|
The cluster typically has hundreds-to-thousands of phones; this prompt
|
||||||
|
asks for *aggregations and anomalies*, not a flat dump.
|
||||||
|
|
||||||
|
## Step 1 — Aggregate counts (always run first)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT tm.name AS model, COUNT(*) AS phone_count
|
||||||
|
FROM device d
|
||||||
|
JOIN typeclass tc ON d.tkclass = tc.enum
|
||||||
|
LEFT OUTER JOIN typemodel tm ON d.tkmodel = tm.enum
|
||||||
|
WHERE tc.name = 'Phone'{name_clause}
|
||||||
|
GROUP BY tm.name ORDER BY 2 DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT dp.name AS device_pool, COUNT(*) AS phone_count
|
||||||
|
FROM device d
|
||||||
|
JOIN typeclass tc ON d.tkclass = tc.enum
|
||||||
|
LEFT OUTER JOIN devicepool dp ON d.fkdevicepool = dp.pkid
|
||||||
|
WHERE tc.name = 'Phone'{name_clause}
|
||||||
|
GROUP BY dp.name ORDER BY 2 DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT css.name AS css_name, COUNT(*) AS phone_count
|
||||||
|
FROM device d
|
||||||
|
JOIN typeclass tc ON d.tkclass = tc.enum
|
||||||
|
LEFT OUTER JOIN callingsearchspace css ON d.fkcallingsearchspace = css.pkid
|
||||||
|
WHERE tc.name = 'Phone'{name_clause}
|
||||||
|
GROUP BY css.name ORDER BY 2 DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
These three give the shape of the fleet. The CSS one in particular is
|
||||||
|
audit-critical: most phones should land in a small number of CSSs
|
||||||
|
(typically `National-CSS` for outbound dialing on this kind of cluster).
|
||||||
|
*Outliers* in the CSS distribution warrant individual inspection.
|
||||||
|
|
||||||
|
## Step 2 — Anomaly queries (the audit-actionable findings)
|
||||||
|
|
||||||
|
### Phones with no description or auto-generated descriptions
|
||||||
|
```sql
|
||||||
|
SELECT FIRST 50 d.name, d.description, dp.name AS device_pool
|
||||||
|
FROM device d
|
||||||
|
JOIN typeclass tc ON d.tkclass = tc.enum
|
||||||
|
LEFT OUTER JOIN devicepool dp ON d.fkdevicepool = dp.pkid
|
||||||
|
WHERE tc.name = 'Phone'{name_clause}
|
||||||
|
AND (d.description IS NULL
|
||||||
|
OR d.description = ''
|
||||||
|
OR d.description = d.name
|
||||||
|
OR d.description LIKE 'AN%' || SUBSTRING(d.name FROM 3) || '%')
|
||||||
|
ORDER BY d.name;
|
||||||
|
```
|
||||||
|
|
||||||
|
(Phones whose description matches or echoes their MAC address /
|
||||||
|
auto-generated name. Hospital deployments accumulate these as patient-
|
||||||
|
room or mobile carts that were quickly added without operator notes.)
|
||||||
|
|
||||||
|
### Phones with no associated owner
|
||||||
|
```sql
|
||||||
|
SELECT FIRST 50 d.name, d.description, dp.name AS device_pool
|
||||||
|
FROM device d
|
||||||
|
JOIN typeclass tc ON d.tkclass = tc.enum
|
||||||
|
LEFT OUTER JOIN devicepool dp ON d.fkdevicepool = dp.pkid
|
||||||
|
WHERE tc.name = 'Phone'{name_clause}
|
||||||
|
AND d.fkenduser IS NULL
|
||||||
|
ORDER BY d.name;
|
||||||
|
```
|
||||||
|
|
||||||
|
(Some unassigned phones are intentional — common areas, guest phones,
|
||||||
|
patient rooms. Operator should verify that the count matches expected
|
||||||
|
shared-use endpoints.)
|
||||||
|
|
||||||
|
### Phones in non-default CSS (where "default" means most-common)
|
||||||
|
After Step 1's CSS distribution query, identify the most common CSS,
|
||||||
|
then list phones NOT in it:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT FIRST 50 d.name, d.description, css.name AS css_name
|
||||||
|
FROM device d
|
||||||
|
JOIN typeclass tc ON d.tkclass = tc.enum
|
||||||
|
LEFT OUTER JOIN callingsearchspace css ON d.fkcallingsearchspace = css.pkid
|
||||||
|
WHERE tc.name = 'Phone'{name_clause}
|
||||||
|
AND (css.name != '<MOST_COMMON_CSS>' OR css.name IS NULL)
|
||||||
|
ORDER BY css.name, d.name;
|
||||||
|
```
|
||||||
|
|
||||||
|
(Replace `<MOST_COMMON_CSS>` with the result from Step 1.)
|
||||||
|
|
||||||
|
### Phones with no primary CSS (NULL)
|
||||||
|
```sql
|
||||||
|
SELECT d.name, d.description, dp.name AS device_pool
|
||||||
|
FROM device d
|
||||||
|
JOIN typeclass tc ON d.tkclass = tc.enum
|
||||||
|
LEFT OUTER JOIN devicepool dp ON d.fkdevicepool = dp.pkid
|
||||||
|
WHERE tc.name = 'Phone'{name_clause} AND d.fkcallingsearchspace IS NULL
|
||||||
|
ORDER BY d.name;
|
||||||
|
```
|
||||||
|
|
||||||
|
(NULL CSS means the phone inherits from device pool. Usually fine, but
|
||||||
|
worth confirming the count matches expectations — a sudden increase
|
||||||
|
indicates config drift.)
|
||||||
|
|
||||||
|
## Step 3 — Cross-reference with users (if owners exist)
|
||||||
|
|
||||||
|
For phones with an owner, list owner identities to spot stale
|
||||||
|
assignments (employee left, phone still tagged to them):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT FIRST 50 d.name, d.description, eu.userid, eu.lastname, eu.status
|
||||||
|
FROM device d
|
||||||
|
JOIN typeclass tc ON d.tkclass = tc.enum
|
||||||
|
JOIN enduser eu ON d.fkenduser = eu.pkid
|
||||||
|
WHERE tc.name = 'Phone'{name_clause}
|
||||||
|
ORDER BY d.name;
|
||||||
|
```
|
||||||
|
|
||||||
|
`enduser.status` of `0` = inactive; `1` = active. A phone owned by an
|
||||||
|
inactive user is a finding.
|
||||||
|
|
||||||
|
## Findings to call out
|
||||||
|
|
||||||
|
- **Naming hygiene gaps**: phones with auto-generated or empty descriptions.
|
||||||
|
Recommend: operational follow-up to add room/owner/purpose notes.
|
||||||
|
- **CSS sprawl**: if more than ~3-5 CSSs are in regular use, justify each.
|
||||||
|
Phones in single-purpose CSSs (e.g., `911CER-CSS` with only 9 devices)
|
||||||
|
usually have specific reasons — document them.
|
||||||
|
- **Orphan owners**: phones owned by inactive users. Recommend reassignment
|
||||||
|
or unassignment.
|
||||||
|
- **Model heterogeneity**: a wide spread of phone models indicates ongoing
|
||||||
|
refresh or drift. Worth knowing for firmware/upgrade planning.
|
||||||
|
- **Devicepool concentration**: if 99%+ of phones are in one device pool,
|
||||||
|
that's the pool that matters for any DP-related change. Flag it as a
|
||||||
|
high-blast-radius asset.
|
||||||
|
|
||||||
|
## Suggested follow-up calls
|
||||||
|
|
||||||
|
- `route_devices_using_css(css_name=<each CSS from Step 1>)` to map the
|
||||||
|
full impact of changing any phone-attached CSS.
|
||||||
|
- `axl_describe_table('device')` if the LLM needs additional columns
|
||||||
|
(firmware load, MAC, security profile, etc.).
|
||||||
|
- `axl_sql("SELECT name FROM typemodel WHERE enum IN (...)")` to decode
|
||||||
|
any unfamiliar model codes.
|
||||||
|
|
||||||
|
## Reference: CUCM data dictionary (devices)
|
||||||
|
|
||||||
|
{schema_block}
|
||||||
|
|
||||||
|
Run the queries above and produce a structured inventory report:
|
||||||
|
counts table, distribution tables, anomaly findings with severity, and
|
||||||
|
a recommendations section. Don't enumerate every phone — focus on
|
||||||
|
aggregates and exceptions.
|
||||||
|
"""
|
||||||
204
src/mcp_cucm_axl/prompts/user_audit.py
Normal file
204
src/mcp_cucm_axl/prompts/user_audit.py
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
"""End user + application user audit: roles, group memberships, security."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from ._common import render_schema_block
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..docs_loader import DocsIndex
|
||||||
|
|
||||||
|
|
||||||
|
_KEYWORDS = [
|
||||||
|
"end user", "application user", "directory group", "function role",
|
||||||
|
"role", "permission", "authentication", "ldap",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Focus → rationale + which audit checklist sections to emphasize
|
||||||
|
_FOCUSES = {
|
||||||
|
"full",
|
||||||
|
"admin", # users with admin/serviceability roles
|
||||||
|
"inactive", # users with status != active
|
||||||
|
"app_users", # service accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render(docs: "DocsIndex | None", focus: str = "full") -> str:
|
||||||
|
"""User audit: end users + application users, role assignments,
|
||||||
|
security-relevant findings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
focus: One of "full", "admin", "inactive", "app_users". Tunes
|
||||||
|
which checklist sections the report emphasizes; all queries
|
||||||
|
still run for context.
|
||||||
|
"""
|
||||||
|
if focus not in _FOCUSES:
|
||||||
|
focus = "full"
|
||||||
|
schema_block = render_schema_block(
|
||||||
|
docs, _KEYWORDS, max_chunks=4, max_chars_per_chunk=900
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"""# CUCM User Audit — focus: `{focus}`
|
||||||
|
|
||||||
|
End users (`enduser`), application users (`applicationuser`), and their
|
||||||
|
role assignments via the dirgroup/functionrole join chain. Security and
|
||||||
|
operational hygiene findings.
|
||||||
|
|
||||||
|
## Step 1 — Headcounts
|
||||||
|
|
||||||
|
End users grouped by status:
|
||||||
|
```sql
|
||||||
|
SELECT status, COUNT(*) AS user_count FROM enduser GROUP BY status;
|
||||||
|
```
|
||||||
|
|
||||||
|
Application users (no status column — they're configuration objects, not
|
||||||
|
human accounts):
|
||||||
|
```sql
|
||||||
|
SELECT COUNT(*) AS app_user_count FROM applicationuser;
|
||||||
|
```
|
||||||
|
|
||||||
|
`enduser.status`: `1` = active, `0` = inactive.
|
||||||
|
|
||||||
|
(Informix's `UNION ALL` rejects mixed-type columns even with `CAST`,
|
||||||
|
so two separate queries is the simplest portable form.)
|
||||||
|
|
||||||
|
## Step 2 — End user inventory
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT FIRST 100
|
||||||
|
eu.userid,
|
||||||
|
eu.firstname,
|
||||||
|
eu.lastname,
|
||||||
|
eu.displayname,
|
||||||
|
eu.status,
|
||||||
|
eu.userrank,
|
||||||
|
eu.islocaluser
|
||||||
|
FROM enduser eu
|
||||||
|
ORDER BY eu.lastname, eu.firstname;
|
||||||
|
```
|
||||||
|
|
||||||
|
`islocaluser` = `t` means CCM-local, `f` means LDAP-synced. Mixing both is
|
||||||
|
common (admins local, hospital staff LDAP-synced).
|
||||||
|
`userrank` is a 1-5 integer that gates access to features by rank;
|
||||||
|
elevated ranks (4-5) usually correspond to admin populations.
|
||||||
|
|
||||||
|
## Step 3 — Application users (service accounts)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT au.name, au.userrank, au.isstandard
|
||||||
|
FROM applicationuser au
|
||||||
|
ORDER BY au.userrank DESC, au.name;
|
||||||
|
```
|
||||||
|
|
||||||
|
`isstandard = 't'` is a built-in account (Cisco-shipped); `f` is operator-
|
||||||
|
created. Operator-created application users with high userrank are the
|
||||||
|
audit-critical population (these are typically API-access service accounts).
|
||||||
|
|
||||||
|
## Step 4 — Role assignments
|
||||||
|
|
||||||
|
End users / app users → dirgroups → function roles:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- End users with their group memberships
|
||||||
|
SELECT FIRST 200
|
||||||
|
eu.userid,
|
||||||
|
eu.lastname,
|
||||||
|
dg.name AS dirgroup_name
|
||||||
|
FROM enduser eu
|
||||||
|
JOIN enduserdirgroupmap eudgm ON eudgm.fkenduser = eu.pkid
|
||||||
|
JOIN dirgroup dg ON eudgm.fkdirgroup = dg.pkid
|
||||||
|
WHERE dg.isstandard = 'f' OR dg.name LIKE '%Standard CCM%Admin%' OR dg.name LIKE '%Super%'
|
||||||
|
ORDER BY eu.userid, dg.name;
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- App users with their group memberships (always audit-relevant)
|
||||||
|
SELECT au.name AS app_user, dg.name AS dirgroup_name
|
||||||
|
FROM applicationuser au
|
||||||
|
JOIN applicationuserdirgroupmap audgm ON audgm.fkapplicationuser = au.pkid
|
||||||
|
JOIN dirgroup dg ON audgm.fkdirgroup = dg.pkid
|
||||||
|
ORDER BY au.name, dg.name;
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Dirgroups → function roles (the actual permissions)
|
||||||
|
SELECT dg.name AS group_name, fr.name AS role_name, fr.description
|
||||||
|
FROM dirgroup dg
|
||||||
|
JOIN functionroledirgroupmap frdgm ON frdgm.fkdirgroup = dg.pkid
|
||||||
|
JOIN functionrole fr ON frdgm.fkfunctionrole = fr.pkid
|
||||||
|
WHERE dg.isstandard = 'f' OR dg.name LIKE '%Admin%' OR dg.name LIKE '%Super%'
|
||||||
|
ORDER BY dg.name, fr.name;
|
||||||
|
```
|
||||||
|
|
||||||
|
The audit question is: who has admin-grade role membership, and is each
|
||||||
|
assignment justified?
|
||||||
|
|
||||||
|
## Step 5 — Phone owners (cross-reference)
|
||||||
|
|
||||||
|
Phones tagged to inactive users:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT FIRST 50
|
||||||
|
d.name AS phone_name,
|
||||||
|
d.description,
|
||||||
|
eu.userid AS owner_userid,
|
||||||
|
eu.lastname,
|
||||||
|
eu.status AS owner_status
|
||||||
|
FROM device d
|
||||||
|
JOIN typeclass tc ON d.tkclass = tc.enum
|
||||||
|
JOIN enduser eu ON d.fkenduser = eu.pkid
|
||||||
|
WHERE tc.name = 'Phone' AND eu.status = '0'
|
||||||
|
ORDER BY eu.lastname, d.name;
|
||||||
|
```
|
||||||
|
|
||||||
|
A phone owned by an inactive user (departed employee, expired account)
|
||||||
|
is config drift — either reassign or unassign.
|
||||||
|
|
||||||
|
## Findings to call out
|
||||||
|
|
||||||
|
### Security-critical
|
||||||
|
- **Application users with admin/superuser group membership**: each one
|
||||||
|
is an API key with elevated privileges. Document why each exists.
|
||||||
|
- **End users with `Standard CCM Super Users` or similar privileged
|
||||||
|
group memberships**: these are the people who can write-modify the
|
||||||
|
cluster. Confirm the population matches the documented admin team.
|
||||||
|
- **`islocaluser = 't'` accounts with admin privileges**: bypass LDAP/SSO,
|
||||||
|
may have static passwords. High-priority security review.
|
||||||
|
- **Application users with default Cisco-shipped passwords**: these don't
|
||||||
|
show in the schema (no plaintext access), but the existence of standard
|
||||||
|
app users (e.g., `CCMAdministrator`, `CCMSysUser`) with operator-set
|
||||||
|
passwords is worth a separate check via the GUI or a pen test.
|
||||||
|
|
||||||
|
### Operational hygiene
|
||||||
|
- **Inactive users (status = 0) still in groups**: cleanup candidates.
|
||||||
|
- **Phones owned by inactive users**: reassign or unassign.
|
||||||
|
- **Users with no group membership**: likely just have basic phone
|
||||||
|
access; not a finding by itself but worth confirming if a sudden
|
||||||
|
population appears.
|
||||||
|
|
||||||
|
### Drift indicators
|
||||||
|
- **End users created locally vs LDAP-synced ratio**: if local-user count
|
||||||
|
has grown over time, indicates either an LDAP sync failure or
|
||||||
|
out-of-band account creation.
|
||||||
|
- **Operator-created dirgroups (`isstandard = 'f'`)**: list and verify each
|
||||||
|
has a documented purpose.
|
||||||
|
|
||||||
|
## Suggested follow-up calls
|
||||||
|
|
||||||
|
- For each app user with API/admin access, run
|
||||||
|
`axl_sql("SELECT * FROM applicationuser WHERE name = '<name>'")` to
|
||||||
|
inspect ACL flags (`acl*` columns).
|
||||||
|
- `axl_describe_table('enduser')` for additional fields (manager,
|
||||||
|
department, etc. if LDAP populates them).
|
||||||
|
- `axl_sql("SELECT name, description FROM dirgroup ORDER BY isstandard, name")`
|
||||||
|
to see all groups including standard Cisco-shipped ones.
|
||||||
|
|
||||||
|
## Reference: CUCM data dictionary (users + roles)
|
||||||
|
|
||||||
|
{schema_block}
|
||||||
|
|
||||||
|
Run the queries above and produce a structured findings report. The
|
||||||
|
focus parameter (`{focus}`) means: emphasize the corresponding section
|
||||||
|
in the writeup, but include all sections for context.
|
||||||
|
"""
|
||||||
@ -385,6 +385,46 @@ def sip_trunk_report(name_filter: str | None = None) -> str:
|
|||||||
return _prompts.sip_trunk_report.render(_docs, name_filter)
|
return _prompts.sip_trunk_report.render(_docs, name_filter)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.prompt
|
||||||
|
def phone_inventory_report(filter: str | None = None) -> str:
|
||||||
|
"""Phone fleet audit: counts by model/pool/CSS, anomaly findings,
|
||||||
|
orphan-owner cross-check.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filter: Optional substring (matches device name OR description) to
|
||||||
|
narrow the inventory. Omit to include all phones.
|
||||||
|
"""
|
||||||
|
return _prompts.phone_inventory_report.render(_docs, filter)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.prompt
|
||||||
|
def user_audit(focus: str = "full") -> str:
|
||||||
|
"""End user + application user audit: roles, group memberships,
|
||||||
|
security-relevant findings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
focus: One of "full", "admin", "inactive", "app_users". Tunes
|
||||||
|
which checklist sections the report emphasizes; all queries
|
||||||
|
still run for context.
|
||||||
|
"""
|
||||||
|
return _prompts.user_audit.render(_docs, focus)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.prompt
|
||||||
|
def inbound_did_audit() -> str:
|
||||||
|
"""Inbound DID inventory: XFORM-Inbound-DNIS curated list, executed
|
||||||
|
routing in Internal-PT, spam blocklist, orphan-target cross-check."""
|
||||||
|
return _prompts.inbound_did_audit.render(_docs)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.prompt
|
||||||
|
def hunt_pilot_audit() -> str:
|
||||||
|
"""Hunt pilot audit: queue settings, distribution algorithms, line
|
||||||
|
group membership, dead-pilot detection. Schema-aware (uses
|
||||||
|
huntpilotqueue.fknumplan_pilot, the verified column name)."""
|
||||||
|
return _prompts.hunt_pilot_audit.render(_docs)
|
||||||
|
|
||||||
|
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
# Bootstrap
|
# Bootstrap
|
||||||
# ====================================================================
|
# ====================================================================
|
||||||
|
|||||||
@ -164,4 +164,80 @@ def test_all_prompts_registered_in_server():
|
|||||||
"audit_routing",
|
"audit_routing",
|
||||||
"cucm_sql_help",
|
"cucm_sql_help",
|
||||||
"sip_trunk_report",
|
"sip_trunk_report",
|
||||||
|
"phone_inventory_report",
|
||||||
|
"user_audit",
|
||||||
|
"inbound_did_audit",
|
||||||
|
"hunt_pilot_audit",
|
||||||
}, f"unexpected prompt set: {names}"
|
}, f"unexpected prompt set: {names}"
|
||||||
|
|
||||||
|
|
||||||
|
# ---- new prompts: render-with-and-without-docs smoke tests -----------------
|
||||||
|
|
||||||
|
def test_phone_inventory_report_renders(fake_docs):
|
||||||
|
text = prompts.phone_inventory_report.render(fake_docs)
|
||||||
|
assert "Phone Inventory" in text
|
||||||
|
# Embedded SQL should query the `device` table with tkclass='Phone'
|
||||||
|
assert "tc.name = 'Phone'" in text
|
||||||
|
assert "all phones" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_phone_inventory_report_with_filter(fake_docs):
|
||||||
|
text = prompts.phone_inventory_report.render(fake_docs, "Lobby")
|
||||||
|
assert "%Lobby%" in text
|
||||||
|
assert "narrowed" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_phone_inventory_report_filter_escaped(fake_docs):
|
||||||
|
text = prompts.phone_inventory_report.render(fake_docs, "O'Hara")
|
||||||
|
assert "O''Hara" in text # SQL injection defense
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_audit_default_focus(fake_docs):
|
||||||
|
text = prompts.user_audit.render(fake_docs)
|
||||||
|
assert "focus: `full`" in text
|
||||||
|
assert "applicationuser" in text
|
||||||
|
assert "enduser" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_audit_admin_focus(fake_docs):
|
||||||
|
text = prompts.user_audit.render(fake_docs, "admin")
|
||||||
|
assert "focus: `admin`" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_audit_unknown_focus_falls_back_to_full(fake_docs):
|
||||||
|
text = prompts.user_audit.render(fake_docs, "bogus_value")
|
||||||
|
assert "focus: `full`" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_inbound_did_audit_renders(fake_docs):
|
||||||
|
text = prompts.inbound_did_audit.render(fake_docs)
|
||||||
|
assert "Inbound DID Audit" in text
|
||||||
|
assert "XFORM-Inbound-DNIS" in text
|
||||||
|
assert "PSTN-Screen-PT" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_hunt_pilot_audit_uses_correct_column(fake_docs):
|
||||||
|
"""Hamilton bonus finding: huntpilotqueue joins via fknumplan_pilot,
|
||||||
|
NOT fknumplan. The prompt's embedded SQL must use the correct column
|
||||||
|
or every audit run will silently fail one category."""
|
||||||
|
text = prompts.hunt_pilot_audit.render(fake_docs)
|
||||||
|
assert "fknumplan_pilot" in text, (
|
||||||
|
"hunt_pilot_audit must use the verified column name `fknumplan_pilot`; "
|
||||||
|
"`fknumplan` was the silent-failure version we fixed in Hamilton review"
|
||||||
|
)
|
||||||
|
assert "tkpatternusage = 7" in text # Hunt Pilot type code
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_new_prompts_render_without_docs():
|
||||||
|
"""Graceful degradation: each prompt produces usable output even when
|
||||||
|
the docs index isn't loaded."""
|
||||||
|
for name, fn, args in [
|
||||||
|
("phone_inventory_report", prompts.phone_inventory_report.render, ()),
|
||||||
|
("user_audit", prompts.user_audit.render, ()),
|
||||||
|
("inbound_did_audit", prompts.inbound_did_audit.render, ()),
|
||||||
|
("hunt_pilot_audit", prompts.hunt_pilot_audit.render, ()),
|
||||||
|
]:
|
||||||
|
text = fn(None, *args)
|
||||||
|
assert "cisco-docs index is not loaded" in text, (
|
||||||
|
f"{name} failed graceful degradation"
|
||||||
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user