From 1ca55974536d502caff29a1f8176936998c545e9 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Fri, 10 Jul 2026 22:48:50 +0200 Subject: [PATCH] feat(servicenow): new ServiceNow ITSM integration 31 commands: ticket lifecycle (create/update/resolve/delete, comments, work notes, links, tags, journal notes, attachments), generic table records CRUD and discovery, CMDB/user/group queries, service catalog ordering, standard change from template, AWA queue routing, generic API call, plus get_incidents ingestion with a bundled OCSF mapper. Basic or OAuth 2.0 (password grant) authentication, stdlib-only scripts. Co-Authored-By: Claude Fable 5 --- .../incident-types/servicenow-ticket.yaml | 3 + integrations/servicenow/manifest.yaml | 446 ++++++++++++++++++ .../servicenow/mappers/get_incidents.yaml | 28 ++ .../servicenow/scripts/add_comment.py | 64 +++ integrations/servicenow/scripts/add_link.py | 67 +++ integrations/servicenow/scripts/add_tag.py | 68 +++ .../scripts/create_change_from_template.py | 60 +++ .../servicenow/scripts/create_item_order.py | 78 +++ .../servicenow/scripts/create_record.py | 79 ++++ .../servicenow/scripts/create_ticket.py | 88 ++++ .../servicenow/scripts/delete_file.py | 62 +++ .../servicenow/scripts/delete_record.py | 61 +++ .../servicenow/scripts/delete_ticket.py | 63 +++ .../scripts/document_route_to_queue.py | 64 +++ .../servicenow/scripts/generic_api_call.py | 71 +++ .../servicenow/scripts/get_change_tasks.py | 60 +++ .../servicenow/scripts/get_incidents.py | 96 ++++ .../servicenow/scripts/get_item_details.py | 60 +++ integrations/servicenow/scripts/get_record.py | 67 +++ .../servicenow/scripts/get_table_name.py | 64 +++ integrations/servicenow/scripts/get_ticket.py | 74 +++ .../scripts/get_ticket_attachments.py | 62 +++ .../servicenow/scripts/get_ticket_notes.py | 66 +++ .../servicenow/scripts/list_table_fields.py | 64 +++ .../servicenow/scripts/query_computers.py | 73 +++ .../servicenow/scripts/query_groups.py | 71 +++ .../servicenow/scripts/query_items.py | 63 +++ .../servicenow/scripts/query_table.py | 69 +++ .../servicenow/scripts/query_tickets.py | 69 +++ .../servicenow/scripts/query_users.py | 71 +++ .../servicenow/scripts/test_connection.py | 62 +++ .../servicenow/scripts/update_record.py | 80 ++++ .../servicenow/scripts/update_ticket.py | 93 ++++ .../servicenow/scripts/upload_file.py | 63 +++ 34 files changed, 2629 insertions(+) create mode 100644 integrations/servicenow/incident-types/servicenow-ticket.yaml create mode 100644 integrations/servicenow/manifest.yaml create mode 100644 integrations/servicenow/mappers/get_incidents.yaml create mode 100644 integrations/servicenow/scripts/add_comment.py create mode 100644 integrations/servicenow/scripts/add_link.py create mode 100644 integrations/servicenow/scripts/add_tag.py create mode 100644 integrations/servicenow/scripts/create_change_from_template.py create mode 100644 integrations/servicenow/scripts/create_item_order.py create mode 100644 integrations/servicenow/scripts/create_record.py create mode 100644 integrations/servicenow/scripts/create_ticket.py create mode 100644 integrations/servicenow/scripts/delete_file.py create mode 100644 integrations/servicenow/scripts/delete_record.py create mode 100644 integrations/servicenow/scripts/delete_ticket.py create mode 100644 integrations/servicenow/scripts/document_route_to_queue.py create mode 100644 integrations/servicenow/scripts/generic_api_call.py create mode 100644 integrations/servicenow/scripts/get_change_tasks.py create mode 100644 integrations/servicenow/scripts/get_incidents.py create mode 100644 integrations/servicenow/scripts/get_item_details.py create mode 100644 integrations/servicenow/scripts/get_record.py create mode 100644 integrations/servicenow/scripts/get_table_name.py create mode 100644 integrations/servicenow/scripts/get_ticket.py create mode 100644 integrations/servicenow/scripts/get_ticket_attachments.py create mode 100644 integrations/servicenow/scripts/get_ticket_notes.py create mode 100644 integrations/servicenow/scripts/list_table_fields.py create mode 100644 integrations/servicenow/scripts/query_computers.py create mode 100644 integrations/servicenow/scripts/query_groups.py create mode 100644 integrations/servicenow/scripts/query_items.py create mode 100644 integrations/servicenow/scripts/query_table.py create mode 100644 integrations/servicenow/scripts/query_tickets.py create mode 100644 integrations/servicenow/scripts/query_users.py create mode 100644 integrations/servicenow/scripts/test_connection.py create mode 100644 integrations/servicenow/scripts/update_record.py create mode 100644 integrations/servicenow/scripts/update_ticket.py create mode 100644 integrations/servicenow/scripts/upload_file.py diff --git a/integrations/servicenow/incident-types/servicenow-ticket.yaml b/integrations/servicenow/incident-types/servicenow-ticket.yaml new file mode 100644 index 0000000..82dbead --- /dev/null +++ b/integrations/servicenow/incident-types/servicenow-ticket.yaml @@ -0,0 +1,3 @@ +name: "ServiceNow Ticket" +color: "#81b5a1" +icon: "alert" diff --git a/integrations/servicenow/manifest.yaml b/integrations/servicenow/manifest.yaml new file mode 100644 index 0000000..a31ba8a --- /dev/null +++ b/integrations/servicenow/manifest.yaml @@ -0,0 +1,446 @@ +id: servicenow +name: ServiceNow +version: 1.0.0 +description: "ServiceNow ITSM (Table / Service Catalog / Change Management REST APIs) — ticket lifecycle (create/update/resolve/delete, comments, work notes, links, tags, journal notes), generic table records CRUD, CMDB and user/group queries, service catalog ordering, standard change creation, attachments, work-item queue routing, plus ticket ingestion (get_incidents) with an OCSF mapper. Basic or OAuth 2.0 (password grant) authentication." +changelog: "1.0.0 — Initial release: ticket lifecycle commands (incident, problem, change_request, sc_request, sc_task, sc_req_item), records/table CRUD and discovery, CMDB computer/group/user queries, service catalog (search, details, order), standard change from template + change tasks, attachments (list/upload/delete), queue routing, generic API call, and ticket ingestion with a bundled OCSF mapper." +category: ticketing + +# Per-instance configuration. The base URL is the instance root, e.g. +# https://company.service-now.com (scripts append /api/now, /api/sn_sc, /api/sn_chg_rest). +config_schema: + properties: + url: + type: string + description: "ServiceNow instance URL, e.g. https://company.service-now.com" + auth_type: + type: string + description: "Authentication method: basic (username + password) or oauth (OAuth 2.0 password grant via /oauth_token.do — also requires client_id/client_secret)" + default: basic + username: + type: string + description: "ServiceNow username (needs rest_api_explorer/web_service_admin style roles plus read/write ACLs on the target tables)" + password: + type: string + description: "ServiceNow password" + x-soar-sensitive: true + client_id: + type: string + description: "OAuth client ID (oauth auth_type only — from an Application Registry entry)" + client_secret: + type: string + description: "OAuth client secret (oauth auth_type only)" + x-soar-sensitive: true + api_version: + type: string + description: "Optional REST API version segment (e.g. v2). Leave empty to use the unversioned endpoints." + ticket_type: + type: string + description: "Default ticket table for ticket commands and ingestion: incident, problem, change_request, sc_request, sc_task or sc_req_item" + default: incident + timestamp_field: + type: string + description: "Timestamp field used as the ingestion watermark (e.g. opened_at or sys_created_on)" + default: opened_at + required: + - url + - username + - password + +# Documented for reference; the bundled scripts build the headers themselves. +# Basic: Authorization: Basic base64(username:password) +# OAuth: POST /oauth_token.do (grant_type=password) then Authorization: Bearer +auth: + - id: basic + type: basic + username_field: username + password_field: password + +commands: + # ── Ingestion ───────────────────────────────────────────────────────────── + - id: get_incidents + name: servicenow-get-incidents + description: "Fetch tickets from the configured ticket table for ingestion. Returns {result:[...]}; use result as the alert rule results path." + risk: read + inputs_schema: + properties: + query: { type: string, description: "Extra sysparm_query filter prepended to the watermark clause (e.g. stateNOT IN6,7)" } + created_after: { type: string, description: "Lower bound on the configured timestamp field, ISO8601, epoch ms or 'YYYY-MM-DD HH:MM:SS' (incremental fetch watermark)" } + ticket_type: { type: string, description: "Override the configured ticket table" } + limit: { type: number, description: "Maximum tickets to fetch (default 100)" } + required: [] + outputs_schema: { properties: {} } + ingest: + results_path: result + dedup_key: sys_id + incremental_field: created_after + + # ── Tickets ─────────────────────────────────────────────────────────────── + - id: get_ticket + name: servicenow-get-ticket + description: "Retrieve a ticket by sys_id or number from any ticket table." + risk: read + inputs_schema: + properties: + id: { type: string, description: "Ticket sys_id" } + number: { type: string, description: "Ticket number (e.g. INC0010001) — used when id is empty" } + ticket_type: { type: string, description: "Ticket table: incident, problem, change_request, sc_request, sc_task, sc_req_item (default from instance config)" } + fields: { type: string, description: "Comma-separated list of fields to return (sys_id always included)" } + display_value: { type: string, description: "sysparm_display_value: true, false or all (default false)" } + required: [] + outputs_schema: { properties: {} } + - id: create_ticket + name: servicenow-create-ticket + description: "Create a ticket. Common fields have named inputs; anything else goes through fields/custom_fields." + inputs_schema: + properties: + ticket_type: { type: string, description: "Ticket table (default from instance config)" } + short_description: { type: string, description: "Short description (title)" } + description: { type: string, description: "Full description" } + urgency: { type: string, description: "Urgency (1, 2 or 3)" } + impact: { type: string, description: "Impact (1, 2 or 3)" } + priority: { type: string, description: "Priority (1-Critical … 5-Planning)" } + state: { type: string, description: "State code (numeric, table-specific)" } + category: { type: string, description: "Category" } + subcategory: { type: string, description: "Subcategory" } + caller_id: { type: string, description: "Caller (sys_id or exact user name)" } + assigned_to: { type: string, description: "Assignee (sys_id or exact user name)" } + assignment_group: { type: string, description: "Assignment group (sys_id or exact group name)" } + comments: { type: string, description: "Customer-visible comment to add on creation" } + work_notes: { type: string, description: "Internal work note to add on creation" } + fields: { type: string, description: "Additional fields as name=value;name2=value2" } + custom_fields: { type: string, description: "Custom fields as name=value;name2=value2 (u_ prefix added when missing)" } + input_display_value: { type: boolean, description: "Treat input values as display values (sysparm_input_display_value)" } + required: [] + outputs_schema: { properties: {} } + - id: update_ticket + name: servicenow-update-ticket + description: "Update a ticket by sys_id (state changes, reassignment, resolution fields, any field via fields/custom_fields)." + inputs_schema: + properties: + id: { type: string, description: "Ticket sys_id to update" } + ticket_type: { type: string, description: "Ticket table (default from instance config)" } + short_description: { type: string, description: "Short description (title)" } + description: { type: string, description: "Full description" } + urgency: { type: string, description: "Urgency (1, 2 or 3)" } + impact: { type: string, description: "Impact (1, 2 or 3)" } + priority: { type: string, description: "Priority (1-Critical … 5-Planning)" } + state: { type: string, description: "State code (numeric, table-specific — e.g. incident 6=Resolved, 7=Closed)" } + category: { type: string, description: "Category" } + subcategory: { type: string, description: "Subcategory" } + caller_id: { type: string, description: "Caller (sys_id or exact user name)" } + assigned_to: { type: string, description: "Assignee (sys_id or exact user name)" } + assignment_group: { type: string, description: "Assignment group (sys_id or exact group name)" } + close_code: { type: string, description: "Close code (required by most instances when resolving)" } + close_notes: { type: string, description: "Close notes (required by most instances when resolving)" } + comments: { type: string, description: "Customer-visible comment to add" } + work_notes: { type: string, description: "Internal work note to add" } + fields: { type: string, description: "Additional fields as name=value;name2=value2" } + custom_fields: { type: string, description: "Custom fields as name=value;name2=value2 (u_ prefix added when missing)" } + input_display_value: { type: boolean, description: "Treat input values as display values (sysparm_input_display_value)" } + required: [id] + outputs_schema: { properties: {} } + - id: delete_ticket + name: servicenow-delete-ticket + description: "Delete a ticket by sys_id." + inputs_schema: + properties: + id: { type: string, description: "Ticket sys_id to delete" } + ticket_type: { type: string, description: "Ticket table (default from instance config)" } + required: [id] + outputs_schema: { properties: {} } + - id: query_tickets + name: servicenow-query-tickets + description: "Query tickets with an encoded sysparm_query (e.g. active=true^priority=1^ORDERBYDESCopened_at)." + risk: read + inputs_schema: + properties: + query: { type: string, description: "Encoded sysparm_query" } + ticket_type: { type: string, description: "Ticket table (default from instance config)" } + fields: { type: string, description: "Comma-separated fields to return (sys_id always included)" } + limit: { type: number, description: "Maximum records (default 10)" } + offset: { type: number, description: "Starting record index (default 0)" } + display_value: { type: string, description: "sysparm_display_value: true, false or all (default false)" } + required: [] + outputs_schema: { properties: {} } + - id: add_comment + name: servicenow-add-comment + description: "Add a customer-visible comment or an internal work note to a ticket." + inputs_schema: + properties: + id: { type: string, description: "Ticket sys_id" } + comment: { type: string, description: "Text to add" } + ticket_type: { type: string, description: "Ticket table (default from instance config)" } + work_note: { type: boolean, description: "Add as internal work note instead of customer-visible comment" } + required: [id, comment] + outputs_schema: { properties: {} } + - id: add_link + name: servicenow-add-link + description: "Post a clickable link (with optional text) into a ticket's comments or work notes." + inputs_schema: + properties: + id: { type: string, description: "Ticket sys_id" } + link: { type: string, description: "URL to post" } + text: { type: string, description: "Link text (defaults to the URL)" } + ticket_type: { type: string, description: "Ticket table (default from instance config)" } + work_note: { type: boolean, description: "Post into work notes instead of comments" } + required: [id, link] + outputs_schema: { properties: {} } + - id: add_tag + name: servicenow-add-tag + description: "Attach an existing tag (label) to a ticket via the label_entry table." + inputs_schema: + properties: + id: { type: string, description: "Ticket sys_id" } + tag_id: { type: string, description: "Tag (label) sys_id — find it with servicenow-query-table on the label table" } + title: { type: string, description: "Entry title, e.g. 'Incident - INC0010001'" } + ticket_type: { type: string, description: "Ticket table (default from instance config)" } + required: [id, tag_id, title] + outputs_schema: { properties: {} } + - id: get_ticket_notes + name: servicenow-get-ticket-notes + description: "Retrieve a ticket's comments and work notes from the journal (requires read access to sys_journal_field)." + risk: read + inputs_schema: + properties: + id: { type: string, description: "Ticket sys_id" } + limit: { type: number, description: "Maximum notes (default 10)" } + offset: { type: number, description: "Starting record index (default 0)" } + required: [id] + outputs_schema: { properties: {} } + - id: get_ticket_attachments + name: servicenow-get-ticket-attachments + description: "List attachment metadata (name, size, content type, download link) for a record." + risk: read + inputs_schema: + properties: + id: { type: string, description: "Record sys_id the attachments belong to" } + required: [id] + outputs_schema: { properties: {} } + - id: upload_file + name: servicenow-upload-file + description: "Attach a file to a record (content provided as base64)." + inputs_schema: + properties: + id: { type: string, description: "Record sys_id to attach the file to" } + file_name: { type: string, description: "File name (extension drives the content type)" } + content_base64: { type: string, description: "File content, base64-encoded" } + table_name: { type: string, description: "Table of the record (default from the instance ticket_type)" } + required: [id, file_name, content_base64] + outputs_schema: { properties: {} } + - id: delete_file + name: servicenow-delete-file + description: "Delete an attachment by its attachment sys_id." + inputs_schema: + properties: + file_sys_id: { type: string, description: "Attachment sys_id (from servicenow-get-ticket-attachments)" } + required: [file_sys_id] + outputs_schema: { properties: {} } + + # ── Records / tables ────────────────────────────────────────────────────── + - id: get_record + name: servicenow-get-record + description: "Retrieve a single record from any table by sys_id." + risk: read + inputs_schema: + properties: + table_name: { type: string, description: "Table name (e.g. alm_asset)" } + id: { type: string, description: "Record sys_id" } + fields: { type: string, description: "Comma-separated fields to return (sys_id always included)" } + display_value: { type: string, description: "sysparm_display_value: true, false or all (default false)" } + required: [table_name, id] + outputs_schema: { properties: {} } + - id: create_record + name: servicenow-create-record + description: "Create a record in any table." + inputs_schema: + properties: + table_name: { type: string, description: "Table name" } + fields: { type: string, description: "Fields as name=value;name2=value2" } + custom_fields: { type: string, description: "Custom fields as name=value;name2=value2 (u_ prefix added when missing)" } + input_display_value: { type: boolean, description: "Treat input values as display values (sysparm_input_display_value)" } + required: [table_name] + outputs_schema: { properties: {} } + - id: update_record + name: servicenow-update-record + description: "Update a record in any table by sys_id." + inputs_schema: + properties: + table_name: { type: string, description: "Table name" } + id: { type: string, description: "Record sys_id" } + fields: { type: string, description: "Fields as name=value;name2=value2" } + custom_fields: { type: string, description: "Custom fields as name=value;name2=value2 (u_ prefix added when missing)" } + input_display_value: { type: boolean, description: "Treat input values as display values (sysparm_input_display_value)" } + required: [table_name, id] + outputs_schema: { properties: {} } + - id: delete_record + name: servicenow-delete-record + description: "Delete a record from any table by sys_id." + inputs_schema: + properties: + table_name: { type: string, description: "Table name" } + id: { type: string, description: "Record sys_id" } + required: [table_name, id] + outputs_schema: { properties: {} } + - id: query_table + name: servicenow-query-table + description: "Query any table with an encoded sysparm_query." + risk: read + inputs_schema: + properties: + table_name: { type: string, description: "Table name" } + query: { type: string, description: "Encoded sysparm_query" } + fields: { type: string, description: "Comma-separated fields to return (sys_id always included)" } + limit: { type: number, description: "Maximum records (default 10)" } + offset: { type: number, description: "Starting record index (default 0)" } + display_value: { type: string, description: "sysparm_display_value: true, false or all (default false)" } + required: [table_name] + outputs_schema: { properties: {} } + - id: list_table_fields + name: servicenow-list-table-fields + description: "List the API field names of a table (from a sample record)." + risk: read + inputs_schema: + properties: + table_name: { type: string, description: "Table name" } + required: [table_name] + outputs_schema: { properties: {} } + - id: get_table_name + name: servicenow-get-table-name + description: "Resolve table names from a display label (e.g. label 'Asset' → alm_asset) via sys_db_object." + risk: read + inputs_schema: + properties: + label: { type: string, description: "Table display label (e.g. Asset, Incident, IP address)" } + limit: { type: number, description: "Maximum results (default 10)" } + required: [label] + outputs_schema: { properties: {} } + + # ── CMDB / users / groups ───────────────────────────────────────────────── + - id: query_computers + name: servicenow-query-computers + description: "Query the cmdb_ci_computer table by sys_id, name, asset tag or encoded query." + risk: read + inputs_schema: + properties: + computer_id: { type: string, description: "Computer sys_id" } + computer_name: { type: string, description: "Exact computer name" } + asset_tag: { type: string, description: "Asset tag" } + query: { type: string, description: "Encoded sysparm_query (used when no other filter is given)" } + limit: { type: number, description: "Maximum records (default 10)" } + offset: { type: number, description: "Starting record index (default 0)" } + required: [] + outputs_schema: { properties: {} } + - id: query_groups + name: servicenow-query-groups + description: "Query the sys_user_group table by sys_id, name or encoded query." + risk: read + inputs_schema: + properties: + group_id: { type: string, description: "Group sys_id" } + group_name: { type: string, description: "Exact group name" } + query: { type: string, description: "Encoded sysparm_query (used when no other filter is given)" } + limit: { type: number, description: "Maximum records (default 10)" } + offset: { type: number, description: "Starting record index (default 0)" } + required: [] + outputs_schema: { properties: {} } + - id: query_users + name: servicenow-query-users + description: "Query the sys_user table by sys_id, username or encoded query." + risk: read + inputs_schema: + properties: + user_id: { type: string, description: "User sys_id" } + user_name: { type: string, description: "Exact username (user_name field)" } + query: { type: string, description: "Encoded sysparm_query (used when no other filter is given)" } + limit: { type: number, description: "Maximum records (default 10)" } + offset: { type: number, description: "Starting record index (default 0)" } + required: [] + outputs_schema: { properties: {} } + + # ── Service catalog ─────────────────────────────────────────────────────── + - id: query_items + name: servicenow-query-items + description: "Search service catalog items by name (Service Catalog API)." + risk: read + inputs_schema: + properties: + name: { type: string, description: "Free-text search (partial match)" } + limit: { type: number, description: "Maximum items (default 10)" } + offset: { type: number, description: "Starting record index (default 0)" } + required: [] + outputs_schema: { properties: {} } + - id: get_item_details + name: servicenow-get-item-details + description: "Get a catalog item's details, including its order variables (Service Catalog API)." + risk: read + inputs_schema: + properties: + id: { type: string, description: "Catalog item sys_id" } + required: [id] + outputs_schema: { properties: {} } + - id: create_item_order + name: servicenow-create-item-order + description: "Order a catalog item (order_now). Mandatory item variables must be provided." + inputs_schema: + properties: + id: { type: string, description: "Catalog item sys_id" } + quantity: { type: number, description: "Quantity to order (default 1)" } + variables: { type: string, description: "Item variables as name=value;name2=value2 (see servicenow-get-item-details)" } + required: [id] + outputs_schema: { properties: {} } + + # ── Change management ───────────────────────────────────────────────────── + - id: create_change_from_template + name: servicenow-create-change-from-template + description: "Create a standard change request from a standard change template (Change Management API)." + inputs_schema: + properties: + template: { type: string, description: "Standard change template sys_id" } + required: [template] + outputs_schema: { properties: {} } + - id: get_change_tasks + name: servicenow-get-change-tasks + description: "List the tasks of a change request (Change Management API)." + risk: read + inputs_schema: + properties: + id: { type: string, description: "Change request sys_id" } + required: [id] + outputs_schema: { properties: {} } + + # ── Misc ────────────────────────────────────────────────────────────────── + - id: document_route_to_queue + name: servicenow-document-route-to-queue + description: "Route a document (ticket/record) to an Advanced Work Assignment queue." + inputs_schema: + properties: + queue_id: { type: string, description: "Queue sys_id (awa_queue table)" } + document_id: { type: string, description: "Document sys_id to route" } + document_table: { type: string, description: "Document table (default incident)" } + required: [queue_id, document_id] + outputs_schema: { properties: {} } + - id: generic_api_call + name: servicenow-generic-api-call + description: "Call any instance REST endpoint (path relative to the instance URL, e.g. /api/now/table/incident)." + inputs_schema: + properties: + method: { type: string, description: "HTTP method: GET, POST, PATCH, PUT or DELETE" } + path: { type: string, description: "Endpoint path starting with / (e.g. /api/now/table/incident)" } + params: { type: string, description: "Query parameters as JSON object (e.g. {\"sysparm_limit\":\"5\"})" } + body: { type: string, description: "Request body as JSON (POST/PATCH/PUT)" } + required: [method, path] + outputs_schema: { properties: {} } + + - id: test_connection + name: servicenow-test-connection + description: "Verify connectivity and credentials (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + +ingestion: + command: get_incidents + mapper: get_incidents + default_incident_type: "ServiceNow Ticket" diff --git a/integrations/servicenow/mappers/get_incidents.yaml b/integrations/servicenow/mappers/get_incidents.yaml new file mode 100644 index 0000000..29d7e40 --- /dev/null +++ b/integrations/servicenow/mappers/get_incidents.yaml @@ -0,0 +1,28 @@ +name: "ServiceNow Tickets → OCSF" +description: "Maps a ServiceNow ticket (Table API, results_path = result) to OCSF finding fields. Reference fields (assigned_to, caller_id, cmdb_ci, …) are {value, link} objects in raw mode and {value, display_value} objects when the fetch uses sysparm_display_value=all — both path variants are listed and absent ones are skipped safely." +field_mappings: + title: "short_description" + severity: "priority = '1' ? 5 : (priority = '2' ? 4 : (priority = '3' ? 3 : 2))" + description: "description" +# results_path = result; source_path is JSONata over ONE ticket object. +# Paths absent from a given ticket are skipped at ingestion, so extra entries are safe. +ocsf: + # ── Finding ─────────────────────────────────────────────────────── + - { source_path: "sys_id", ocsf_field: "finding_info.uid" } + - { source_path: "number", ocsf_field: "finding_info.uid_alt" } + - { source_path: "short_description", ocsf_field: "finding_info.title" } + - { source_path: "description", ocsf_field: "finding_info.desc" } + - { source_path: "opened_at", ocsf_field: "finding_info.first_seen_time" } + - { source_path: "sys_created_on", ocsf_field: "finding_info.created_time" } + - { source_path: "sys_updated_on", ocsf_field: "finding_info.modified_time" } + - { source_path: "category", ocsf_field: "finding_info.analytic.category" } + - { source_path: "subcategory", ocsf_field: "finding_info.types" } + - { source_path: "state", ocsf_field: "status" } + - { source_path: "close_code", ocsf_field: "disposition" } + - { source_path: "close_notes", ocsf_field: "comment" } + # ── People ──────────────────────────────────────────────────────── + - { source_path: "sys_created_by", ocsf_field: "actor.user.name" } + - { source_path: "caller_id.display_value", ocsf_field: "user.name" } + - { source_path: "assigned_to.display_value", ocsf_field: "assignee.name" } + # ── Affected CI ─────────────────────────────────────────────────── + - { source_path: "cmdb_ci.display_value", ocsf_field: "device.hostname" } diff --git a/integrations/servicenow/scripts/add_comment.py b/integrations/servicenow/scripts/add_comment.py new file mode 100644 index 0000000..754e856 --- /dev/null +++ b/integrations/servicenow/scripts/add_comment.py @@ -0,0 +1,64 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + ticket_id = str(inputs.get("id") or "") + if not ticket_id: + raise Exception("id is required") + table = str(inputs.get("ticket_type") or _cfg().get("ticket_type") or "incident") + key = "work_notes" if inputs.get("work_note") else "comments" + body = {key: str(inputs.get("comment") or "")} + print(json.dumps(request("PATCH", "/table/" + urllib.parse.quote(table) + "/" + urllib.parse.quote(ticket_id), body=body))) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/add_link.py b/integrations/servicenow/scripts/add_link.py new file mode 100644 index 0000000..0cfd02f --- /dev/null +++ b/integrations/servicenow/scripts/add_link.py @@ -0,0 +1,67 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + ticket_id = str(inputs.get("id") or "") + if not ticket_id: + raise Exception("id is required") + table = str(inputs.get("ticket_type") or _cfg().get("ticket_type") or "incident") + link = str(inputs.get("link") or "") + text = str(inputs.get("text") or link) + html = '[code]' + text + '[/code]' + key = "work_notes" if inputs.get("work_note") else "comments" + body = {key: html} + print(json.dumps(request("PATCH", "/table/" + urllib.parse.quote(table) + "/" + urllib.parse.quote(ticket_id), body=body))) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/add_tag.py b/integrations/servicenow/scripts/add_tag.py new file mode 100644 index 0000000..719c723 --- /dev/null +++ b/integrations/servicenow/scripts/add_tag.py @@ -0,0 +1,68 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + ticket_id = str(inputs.get("id") or "") + if not ticket_id: + raise Exception("id is required") + table = str(inputs.get("ticket_type") or _cfg().get("ticket_type") or "incident") + body = { + "label": str(inputs.get("tag_id") or ""), + "table": table, + "table_key": ticket_id, + "title": str(inputs.get("title") or ""), + } + print(json.dumps(request("POST", "/table/label_entry", body=body))) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/create_change_from_template.py b/integrations/servicenow/scripts/create_change_from_template.py new file mode 100644 index 0000000..1e3284c --- /dev/null +++ b/integrations/servicenow/scripts/create_change_from_template.py @@ -0,0 +1,60 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + template = inputs.get("template") or "" + res = request("POST", "/change/standard/" + urllib.parse.quote(template), root="/api/sn_chg_rest", body={}) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/create_item_order.py b/integrations/servicenow/scripts/create_item_order.py new file mode 100644 index 0000000..5614d8b --- /dev/null +++ b/integrations/servicenow/scripts/create_item_order.py @@ -0,0 +1,78 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def split_fields(s, prefix_custom=False): + out = {} + for part in str(s or "").split(";"): + if "=" not in part: + continue + k, v = part.split("=", 1) + k = k.strip() + if prefix_custom and k and not k.startswith("u_"): + k = "u_" + k + if k: + out[k] = v + return out + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + iid = inputs.get("id") or "" + body = { + "sysparm_quantity": str(int(inputs.get("quantity") or 1)), + "variables": split_fields(inputs.get("variables")), + } + res = request("POST", "/servicecatalog/items/" + urllib.parse.quote(iid) + "/order_now", root="/api/sn_sc", body=body) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/create_record.py b/integrations/servicenow/scripts/create_record.py new file mode 100644 index 0000000..7280b21 --- /dev/null +++ b/integrations/servicenow/scripts/create_record.py @@ -0,0 +1,79 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def split_fields(s, prefix_custom=False): + out = {} + for part in str(s or "").split(";"): + if "=" not in part: + continue + k, v = part.split("=", 1) + k = k.strip() + if prefix_custom and k and not k.startswith("u_"): + k = "u_" + k + if k: + out[k] = v + return out + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + table = inputs.get("table_name") or "" + body = split_fields(inputs.get("fields")) + body.update(split_fields(inputs.get("custom_fields"), prefix_custom=True)) + if not body: + raise Exception("no fields to set") + params = {"sysparm_input_display_value": "true"} if inputs.get("input_display_value") else None + res = request("POST", "/table/" + urllib.parse.quote(table), params=params, body=body) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/create_ticket.py b/integrations/servicenow/scripts/create_ticket.py new file mode 100644 index 0000000..6462671 --- /dev/null +++ b/integrations/servicenow/scripts/create_ticket.py @@ -0,0 +1,88 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def split_fields(s, prefix_custom=False): + out = {} + for part in str(s or "").split(";"): + if "=" not in part: + continue + k, v = part.split("=", 1) + k = k.strip() + if prefix_custom and k and not k.startswith("u_"): + k = "u_" + k + if k: + out[k] = v + return out + + +NAMED_FIELDS = [ + "short_description", "description", "urgency", "impact", "priority", "state", + "category", "subcategory", "caller_id", "assigned_to", "assignment_group", + "comments", "work_notes", +] + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + table = str(inputs.get("ticket_type") or _cfg().get("ticket_type") or "incident") + body = {} + for k in NAMED_FIELDS: + v = inputs.get(k) + if v not in (None, ""): + body[k] = v + body.update(split_fields(inputs.get("fields"))) + body.update(split_fields(inputs.get("custom_fields"), prefix_custom=True)) + params = {"sysparm_input_display_value": "true"} if inputs.get("input_display_value") else None + print(json.dumps(request("POST", "/table/" + urllib.parse.quote(table), params=params, body=body))) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/delete_file.py b/integrations/servicenow/scripts/delete_file.py new file mode 100644 index 0000000..362e8f8 --- /dev/null +++ b/integrations/servicenow/scripts/delete_file.py @@ -0,0 +1,62 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_sys_id = str(inputs.get("file_sys_id") or "") + if not file_sys_id: + raise Exception("file_sys_id is required") + res = request("DELETE", "/attachment/" + urllib.parse.quote(file_sys_id), versioned=False) + print(json.dumps(res if res else {"ok": True, "file_sys_id": file_sys_id})) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/delete_record.py b/integrations/servicenow/scripts/delete_record.py new file mode 100644 index 0000000..8c99d44 --- /dev/null +++ b/integrations/servicenow/scripts/delete_record.py @@ -0,0 +1,61 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + table = inputs.get("table_name") or "" + rid = inputs.get("id") or "" + res = request("DELETE", "/table/" + urllib.parse.quote(table) + "/" + urllib.parse.quote(rid)) + print(json.dumps(res if res else {"ok": True, "id": rid})) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/delete_ticket.py b/integrations/servicenow/scripts/delete_ticket.py new file mode 100644 index 0000000..c3697da --- /dev/null +++ b/integrations/servicenow/scripts/delete_ticket.py @@ -0,0 +1,63 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + ticket_id = str(inputs.get("id") or "") + if not ticket_id: + raise Exception("id is required") + table = str(inputs.get("ticket_type") or _cfg().get("ticket_type") or "incident") + res = request("DELETE", "/table/" + urllib.parse.quote(table) + "/" + urllib.parse.quote(ticket_id)) + print(json.dumps(res if res else {"ok": True, "id": ticket_id})) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/document_route_to_queue.py b/integrations/servicenow/scripts/document_route_to_queue.py new file mode 100644 index 0000000..cf55424 --- /dev/null +++ b/integrations/servicenow/scripts/document_route_to_queue.py @@ -0,0 +1,64 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + queue_id = inputs.get("queue_id") or "" + body = { + "document_sys_id": inputs.get("document_id") or "", + "document_table": inputs.get("document_table") or "incident", + } + res = request("POST", "/awa/queues/" + urllib.parse.quote(queue_id) + "/work_item", versioned=False, body=body) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/generic_api_call.py b/integrations/servicenow/scripts/generic_api_call.py new file mode 100644 index 0000000..fc8be98 --- /dev/null +++ b/integrations/servicenow/scripts/generic_api_call.py @@ -0,0 +1,71 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def main(): + cfg = _cfg() + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + method = str(inputs.get("method") or "GET").upper() + if method not in ("GET", "POST", "PATCH", "PUT", "DELETE"): + raise Exception("Unsupported method: " + method) + path = str(inputs.get("path") or "") + if not path.startswith("/"): + raise Exception("path must start with /") + + url = cfg.get("url", "").rstrip("/") + path + params = inputs.get("params") + if params: + if isinstance(params, str): + params = json.loads(params) + q = {k: str(v) for k, v in params.items() if v not in (None, "")} + if q: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + + body = inputs.get("body") + data = None + if body not in (None, ""): + if isinstance(body, str): + body = json.loads(body) + data = json.dumps(body).encode("utf-8") + + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + print(json.dumps(json.loads(raw) if raw else {})) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/get_change_tasks.py b/integrations/servicenow/scripts/get_change_tasks.py new file mode 100644 index 0000000..e60343b --- /dev/null +++ b/integrations/servicenow/scripts/get_change_tasks.py @@ -0,0 +1,60 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + cid = inputs.get("id") or "" + res = request("GET", "/change/" + urllib.parse.quote(cid) + "/task", root="/api/sn_chg_rest") + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/get_incidents.py b/integrations/servicenow/scripts/get_incidents.py new file mode 100644 index 0000000..e6b16c1 --- /dev/null +++ b/integrations/servicenow/scripts/get_incidents.py @@ -0,0 +1,96 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error +from datetime import datetime, timezone + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def to_snow_time(v): + """Normalize ISO8601 / epoch (s or ms) / native format to 'YYYY-MM-DD HH:MM:SS' UTC.""" + s = str(v or "").strip() + if not s: + return None + if s.isdigit(): + ts = int(s) + if ts > 10**12: + ts //= 1000 + return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + if len(s) == 19 and s[4] == "-" and s[10] == " ": + return s + try: + dt = datetime.fromisoformat(s.replace("Z", "+00:00")) + if dt.tzinfo is not None: + dt = dt.astimezone(timezone.utc) + return dt.strftime("%Y-%m-%d %H:%M:%S") + except Exception: + return s + + +def main(): + cfg = _cfg() + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + table = str(inputs.get("ticket_type") or cfg.get("ticket_type") or "incident") + ts_field = str(cfg.get("timestamp_field") or "opened_at") + limit = int(inputs.get("limit") or 100) + + parts = [] + if inputs.get("query"): + parts.append(str(inputs["query"])) + watermark = to_snow_time(inputs.get("created_after")) + if watermark: + parts.append(ts_field + ">" + watermark) + parts.append("ORDERBY" + ts_field) + + print(json.dumps(request("GET", "/table/" + urllib.parse.quote(table), params={ + "sysparm_query": "^".join(parts), + "sysparm_limit": limit, + }))) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/get_item_details.py b/integrations/servicenow/scripts/get_item_details.py new file mode 100644 index 0000000..51835c3 --- /dev/null +++ b/integrations/servicenow/scripts/get_item_details.py @@ -0,0 +1,60 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + iid = inputs.get("id") or "" + res = request("GET", "/servicecatalog/items/" + urllib.parse.quote(iid), root="/api/sn_sc") + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/get_record.py b/integrations/servicenow/scripts/get_record.py new file mode 100644 index 0000000..110978f --- /dev/null +++ b/integrations/servicenow/scripts/get_record.py @@ -0,0 +1,67 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + table = inputs.get("table_name") or "" + rid = inputs.get("id") or "" + fields = str(inputs.get("fields") or "").strip() + if fields and "sys_id" not in [x.strip() for x in fields.split(",")]: + fields += ",sys_id" + res = request("GET", "/table/" + urllib.parse.quote(table) + "/" + urllib.parse.quote(rid), params={ + "sysparm_fields": fields or None, + "sysparm_display_value": inputs.get("display_value"), + }) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/get_table_name.py b/integrations/servicenow/scripts/get_table_name.py new file mode 100644 index 0000000..f8ea4f2 --- /dev/null +++ b/integrations/servicenow/scripts/get_table_name.py @@ -0,0 +1,64 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + label = inputs.get("label") or "" + res = request("GET", "/table/sys_db_object", params={ + "sysparm_query": "label=" + label, + "sysparm_limit": inputs.get("limit") or 10, + "sysparm_fields": "sys_id,name,label,sys_name", + }) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/get_ticket.py b/integrations/servicenow/scripts/get_ticket.py new file mode 100644 index 0000000..da1af89 --- /dev/null +++ b/integrations/servicenow/scripts/get_ticket.py @@ -0,0 +1,74 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + table = str(inputs.get("ticket_type") or _cfg().get("ticket_type") or "incident") + fields = str(inputs.get("fields") or "") + if fields and "sys_id" not in fields: + fields += ",sys_id" + params = { + "sysparm_fields": fields or None, + "sysparm_display_value": inputs.get("display_value"), + } + if inputs.get("id"): + res = request("GET", "/table/" + urllib.parse.quote(table) + "/" + urllib.parse.quote(str(inputs["id"])), params=params) + elif inputs.get("number"): + params["sysparm_query"] = "number=" + str(inputs["number"]) + params["sysparm_limit"] = 1 + res = request("GET", "/table/" + urllib.parse.quote(table), params=params) + else: + raise Exception("id or number is required") + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/get_ticket_attachments.py b/integrations/servicenow/scripts/get_ticket_attachments.py new file mode 100644 index 0000000..8dff4d0 --- /dev/null +++ b/integrations/servicenow/scripts/get_ticket_attachments.py @@ -0,0 +1,62 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + ticket_id = str(inputs.get("id") or "") + if not ticket_id: + raise Exception("id is required") + params = {"sysparm_query": "table_sys_id=" + ticket_id} + print(json.dumps(request("GET", "/attachment", params=params, versioned=False))) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/get_ticket_notes.py b/integrations/servicenow/scripts/get_ticket_notes.py new file mode 100644 index 0000000..98aa2ec --- /dev/null +++ b/integrations/servicenow/scripts/get_ticket_notes.py @@ -0,0 +1,66 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + ticket_id = str(inputs.get("id") or "") + if not ticket_id: + raise Exception("id is required") + params = { + "sysparm_query": "element_id=" + ticket_id + "^element=comments^ORelement=work_notes^ORDERBYsys_created_on", + "sysparm_limit": inputs.get("limit") or 10, + "sysparm_offset": inputs.get("offset") or 0, + } + print(json.dumps(request("GET", "/table/sys_journal_field", params=params))) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/list_table_fields.py b/integrations/servicenow/scripts/list_table_fields.py new file mode 100644 index 0000000..af92e6f --- /dev/null +++ b/integrations/servicenow/scripts/list_table_fields.py @@ -0,0 +1,64 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + table = inputs.get("table_name") or "" + res = request("GET", "/table/" + urllib.parse.quote(table), params={"sysparm_limit": 1}) + rows = res.get("result") or [] + if not rows: + print(json.dumps({"table": table, "fields": []})) + return + print(json.dumps({"table": table, "fields": sorted(rows[0].keys())})) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/query_computers.py b/integrations/servicenow/scripts/query_computers.py new file mode 100644 index 0000000..8bfbba2 --- /dev/null +++ b/integrations/servicenow/scripts/query_computers.py @@ -0,0 +1,73 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + cid = inputs.get("computer_id") or "" + if cid: + res = request("GET", "/table/cmdb_ci_computer/" + urllib.parse.quote(cid)) + else: + if inputs.get("computer_name"): + q = "name=" + inputs["computer_name"] + elif inputs.get("asset_tag"): + q = "asset_tag=" + inputs["asset_tag"] + else: + q = inputs.get("query") or "" + res = request("GET", "/table/cmdb_ci_computer", params={ + "sysparm_query": q, + "sysparm_limit": inputs.get("limit") or 10, + "sysparm_offset": inputs.get("offset") or 0, + }) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/query_groups.py b/integrations/servicenow/scripts/query_groups.py new file mode 100644 index 0000000..1f7eee0 --- /dev/null +++ b/integrations/servicenow/scripts/query_groups.py @@ -0,0 +1,71 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + gid = inputs.get("group_id") or "" + if gid: + res = request("GET", "/table/sys_user_group/" + urllib.parse.quote(gid)) + else: + if inputs.get("group_name"): + q = "name=" + inputs["group_name"] + else: + q = inputs.get("query") or "" + res = request("GET", "/table/sys_user_group", params={ + "sysparm_query": q, + "sysparm_limit": inputs.get("limit") or 10, + "sysparm_offset": inputs.get("offset") or 0, + }) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/query_items.py b/integrations/servicenow/scripts/query_items.py new file mode 100644 index 0000000..37868b7 --- /dev/null +++ b/integrations/servicenow/scripts/query_items.py @@ -0,0 +1,63 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + res = request("GET", "/servicecatalog/items", root="/api/sn_sc", params={ + "sysparm_text": inputs.get("name"), + "sysparm_limit": inputs.get("limit") or 10, + "sysparm_offset": inputs.get("offset") or 0, + }) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/query_table.py b/integrations/servicenow/scripts/query_table.py new file mode 100644 index 0000000..ecf53a8 --- /dev/null +++ b/integrations/servicenow/scripts/query_table.py @@ -0,0 +1,69 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + table = inputs.get("table_name") or "" + fields = str(inputs.get("fields") or "").strip() + if fields and "sys_id" not in [x.strip() for x in fields.split(",")]: + fields += ",sys_id" + res = request("GET", "/table/" + urllib.parse.quote(table), params={ + "sysparm_query": inputs.get("query"), + "sysparm_limit": inputs.get("limit") or 10, + "sysparm_offset": inputs.get("offset") or 0, + "sysparm_fields": fields or None, + "sysparm_display_value": inputs.get("display_value"), + }) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/query_tickets.py b/integrations/servicenow/scripts/query_tickets.py new file mode 100644 index 0000000..50534ed --- /dev/null +++ b/integrations/servicenow/scripts/query_tickets.py @@ -0,0 +1,69 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + table = str(inputs.get("ticket_type") or _cfg().get("ticket_type") or "incident") + fields = str(inputs.get("fields") or "") + if fields and "sys_id" not in fields: + fields += ",sys_id" + params = { + "sysparm_query": inputs.get("query"), + "sysparm_limit": inputs.get("limit") or 10, + "sysparm_offset": inputs.get("offset") or 0, + "sysparm_fields": fields or None, + "sysparm_display_value": inputs.get("display_value"), + } + print(json.dumps(request("GET", "/table/" + urllib.parse.quote(table), params=params))) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/query_users.py b/integrations/servicenow/scripts/query_users.py new file mode 100644 index 0000000..f4d4d0a --- /dev/null +++ b/integrations/servicenow/scripts/query_users.py @@ -0,0 +1,71 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + uid = inputs.get("user_id") or "" + if uid: + res = request("GET", "/table/sys_user/" + urllib.parse.quote(uid)) + else: + if inputs.get("user_name"): + q = "user_name=" + inputs["user_name"] + else: + q = inputs.get("query") or "" + res = request("GET", "/table/sys_user", params={ + "sysparm_query": q, + "sysparm_limit": inputs.get("limit") or 10, + "sysparm_offset": inputs.get("offset") or 0, + }) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/test_connection.py b/integrations/servicenow/scripts/test_connection.py new file mode 100644 index 0000000..79e430d --- /dev/null +++ b/integrations/servicenow/scripts/test_connection.py @@ -0,0 +1,62 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + cfg = _cfg() + table = str(cfg.get("ticket_type") or "incident") + res = request("GET", "/table/" + urllib.parse.quote(table), params={"sysparm_limit": 1}) + if "result" not in res: + raise Exception("Unexpected response: " + json.dumps(res)) + print(json.dumps({"ok": True, "table": table})) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/update_record.py b/integrations/servicenow/scripts/update_record.py new file mode 100644 index 0000000..087a9ec --- /dev/null +++ b/integrations/servicenow/scripts/update_record.py @@ -0,0 +1,80 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def split_fields(s, prefix_custom=False): + out = {} + for part in str(s or "").split(";"): + if "=" not in part: + continue + k, v = part.split("=", 1) + k = k.strip() + if prefix_custom and k and not k.startswith("u_"): + k = "u_" + k + if k: + out[k] = v + return out + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + table = inputs.get("table_name") or "" + rid = inputs.get("id") or "" + body = split_fields(inputs.get("fields")) + body.update(split_fields(inputs.get("custom_fields"), prefix_custom=True)) + if not body: + raise Exception("nothing to update") + params = {"sysparm_input_display_value": "true"} if inputs.get("input_display_value") else None + res = request("PATCH", "/table/" + urllib.parse.quote(table) + "/" + urllib.parse.quote(rid), params=params, body=body) + print(json.dumps(res)) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/update_ticket.py b/integrations/servicenow/scripts/update_ticket.py new file mode 100644 index 0000000..c78a1bc --- /dev/null +++ b/integrations/servicenow/scripts/update_ticket.py @@ -0,0 +1,93 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def request(method, path, params=None, body=None, root="/api/now", versioned=True): + cfg = _cfg() + v = str(cfg.get("api_version") or "").strip().strip("/") + url = cfg.get("url", "").rstrip("/") + root + ("/" + v if v and versioned else "") + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if q: + url += "?" + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + req = urllib.request.Request(url, data=data, headers=_headers(cfg), method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def split_fields(s, prefix_custom=False): + out = {} + for part in str(s or "").split(";"): + if "=" not in part: + continue + k, v = part.split("=", 1) + k = k.strip() + if prefix_custom and k and not k.startswith("u_"): + k = "u_" + k + if k: + out[k] = v + return out + + +NAMED_FIELDS = [ + "short_description", "description", "urgency", "impact", "priority", "state", + "category", "subcategory", "caller_id", "assigned_to", "assignment_group", + "comments", "work_notes", "close_code", "close_notes", +] + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + ticket_id = str(inputs.get("id") or "") + if not ticket_id: + raise Exception("id is required") + table = str(inputs.get("ticket_type") or _cfg().get("ticket_type") or "incident") + body = {} + for k in NAMED_FIELDS: + v = inputs.get(k) + if v not in (None, ""): + body[k] = v + body.update(split_fields(inputs.get("fields"))) + body.update(split_fields(inputs.get("custom_fields"), prefix_custom=True)) + if not body: + raise Exception("nothing to update") + params = {"sysparm_input_display_value": "true"} if inputs.get("input_display_value") else None + print(json.dumps(request("PATCH", "/table/" + urllib.parse.quote(table) + "/" + urllib.parse.quote(ticket_id), params=params, body=body))) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) diff --git a/integrations/servicenow/scripts/upload_file.py b/integrations/servicenow/scripts/upload_file.py new file mode 100644 index 0000000..b3ca5a1 --- /dev/null +++ b/integrations/servicenow/scripts/upload_file.py @@ -0,0 +1,63 @@ +import base64, json, mimetypes, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(cfg): + h = {"Accept": "application/json", "Content-Type": "application/json"} + if str(cfg.get("auth_type") or "basic").lower() == "oauth": + data = urllib.parse.urlencode({ + "grant_type": "password", + "client_id": cfg.get("client_id", ""), + "client_secret": cfg.get("client_secret", ""), + "username": cfg.get("username", ""), + "password": cfg.get("password", ""), + }).encode("utf-8") + req = urllib.request.Request( + cfg.get("url", "").rstrip("/") + "/oauth_token.do", data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("OAuth token request failed: " + json.dumps(tok)) + h["Authorization"] = "Bearer " + tok["access_token"] + else: + cred = (cfg.get("username", "") + ":" + cfg.get("password", "")).encode("utf-8") + h["Authorization"] = "Basic " + base64.b64encode(cred).decode("ascii") + return h + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + ticket_id = str(inputs.get("id") or "") + if not ticket_id: + raise Exception("id is required") + cfg = _cfg() + table = str(inputs.get("table_name") or cfg.get("ticket_type") or "incident") + file_name = str(inputs.get("file_name") or "") + if not file_name: + raise Exception("file_name is required") + url = cfg.get("url", "").rstrip("/") + "/api/now/attachment/file?" + urllib.parse.urlencode({ + "table_name": table, + "table_sys_id": ticket_id, + "file_name": file_name, + }) + headers = _headers(cfg) + headers["Content-Type"] = mimetypes.guess_type(file_name)[0] or "application/octet-stream" + body = base64.b64decode(inputs["content_base64"]) + req = urllib.request.Request(url, data=body, headers=headers, method="POST") + with urllib.request.urlopen(req, timeout=120) as r: + raw = r.read() + print(json.dumps(json.loads(raw) if raw else {})) + + +try: + main() +except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) +except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1)