diff --git a/.github/workflows/run-int-dev-checklist.yml b/.github/workflows/run-int-dev-checklist.yml index ffa86d7..80e76bd 100644 --- a/.github/workflows/run-int-dev-checklist.yml +++ b/.github/workflows/run-int-dev-checklist.yml @@ -7,7 +7,7 @@ on: jobs: run-integration-development-checklist: runs-on: ubuntu-latest - container: 'centos:7' + container: 'rockylinux:8' steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 9aac006..b1897f8 100644 --- a/README.md +++ b/README.md @@ -22,21 +22,39 @@ The username of the Service Now user you want the integration to authenticate as ### Password The password for the provided username you want the integration to authenticate as. -### Search By String -This will toggle whether or not to search the ServiceNow's Asset Table with strings found in your channels. +### Search By Annotated Entities +This will toggle whether to search ServiceNow for annotated entities found in your channels. The "string" Data Type must also be enabled for this option to have an effect. + +### Enable Incident Search +If checked, the integration will search ServiceNow's Incident Table (incident) for IP Addresses, Domains, CVEs, annotated entities, and any added custom types ### Incident Query Fields -A comma separated list of Fields to query against Incidents. -> NOTE: If a field is not in this list, it will not be searched on on in ServiceNow's Incident Table. -> (This applies to IP Addresses, Domains, and String searches) +A comma separated list of fields to search when querying for Incidents. Incident searches are done for IPs, domains, CVEs, annotated entities and any added custom types. NOTE: If a field is not in this list, it will not be searched for Incident Queries. + +### Incident Search Window in Days +Number of days back to search when searching incidents. Filters based on the date that the Incident was opened. Defaults to 360. ### Enable Asset Search -If checked, the integration will search ServiceNow's Asset Table (alm_asset) for IP Addresses, Domains, CVEs and annotated entities. +If checked, the integration will search ServiceNow's Asset Table (alm_asset) for IP Addresses, Domains, CVEs, annotated entities, and any added custom types. ### Asset Query Fields -A comma separated list of fields to search domains and IPs by in ServiceNow's Asset Table. -> NOTE: If a field is not in this list, it will not be searched on in ServiceNow's Asset Table. -> (This applies to IP Addresses, Domains, and String searches) +A comma separated list of fields to search when querying for Assets. Asset searches are done for IPs, domains, CVEs, annotated entities and any added custom types. NOTE: If a field is not in this list, the field will not be searched in ServiceNow's Asset Table. + +This option defaults to searching the `ci.name` and `ci.asset_tag` fields. The correct fields to search are dependent on your ServiceNow implementation. A common additional field to add is `comments`. + +## Searching Assets + +Assets in ServiceNow are commonly found in the Assets table `alm_assets` or the Configuration Item table `cmdb_ci`. The Polarity ServiceNow integration searches the `alm_assets` table as part of its asset search capability but fields within the `cmdb_ci` table can be referenced for searching by prepending the table's column name with `ci.`. As an example, if you'd like to search the `asset_tag` field within the `cmdb_ci` table, you should set the "Asset Query Fields" option to `ci.asset_tag`. + +Common "Asset Query Fields" include: + +* display_name +* name +* asset_tag +* comments +* ci.name +* ci.display_name +* ci.asset_tag ## IP Lookups and Finding Query Fields Because ServiceNow is often customized to fit specific needs, Polarity's ServiceNow Integration offers the ability to look up IPv4 matches on custom Incident and Asset fields. Simply add a comma separated list of custom fields to the `Custom Fields` integration option, and when Polarity recognizes an IP address, it will look up the address in the custom fields you listed and display the results. To determine what value you should put in this field your can reference our guide [**Here**](./HowToFindCustomFields.md) using the dashboard. @@ -49,6 +67,14 @@ They are usually prepended with a `u_` and then the name of the custom field, in |---| |*Custom Query Field Example*| +## Known Issues + +If adding additional custom types to the ServiceNow integration, ensure the added custom types do not also match on the built-in custom types for Incidents, Change Requests, Knowledge Base, Request, and Request Item ids. + +As an example, if you add a new custom type that matches on the string `INC0001234`, this will conflict with the integration's built-in custom type for looking up incidents by ID. + +Ensure that newly added custom types (e.g., for hostnames), do not overlap with these custom types. + ## Polarity Polarity is a memory-augmentation platform that improves and accelerates analyst decision making. For more information about the Polarity platform please see: diff --git a/config/config.js b/config/config.js index fd117da..b6402ab 100644 --- a/config/config.js +++ b/config/config.js @@ -1,69 +1,33 @@ module.exports = { - /** - * Name of the integration which is displayed in the Polarity integrations user interface - * - * @type String - * @required - */ name: 'ServiceNow', - /** - * The acronym that appears in the notification window when information from this integration - * is displayed. Note that the acronym is included as part of each "tag" in the summary information - * for the integration. As a result, it is best to keep it to 4 or less characters. The casing used - * here will be carried forward into the notification window. - * - * @type String - * @required - */ acronym: 'SN', defaultColor: 'light-purple', - /** - * Description for this integration which is displayed in the Polarity integrations user interface - * - * @type String - * @optional - */ description: 'ServiceNow automates and streamlines work and helps create great employee and customer experiences.', entityTypes: ['IPv4', 'email', 'domain', 'string', 'cve'], customTypes: [ { key: 'incident', - regex: /INC[0-9]{7,}/ + regex: 'INC[0-9]{7,}' }, { key: 'knowledgeBase', - regex: /KB[0-9]{7,}/ + regex: 'KB[0-9]{7,}' }, { key: 'change', - regex: /CHG[0-9]{7,}/ + regex: 'CHG[0-9]{7,}' }, { key: 'request', - regex: /REQ[0-9]{7,}/ + regex: 'REQ[0-9]{7,}' }, { key: 'requestedItem', - regex: /RITM[0-9]{7,}/ + regex: 'RITM[0-9]{7,}' } ], - /** - * An array of style files (css or less) that will be included for your integration. Any styles specified in - * the below files can be used in your custom template. - * - * @type Array - * @optional - */ styles: ['./styles/styles.less'], - /** - * Provide custom component logic and template for rendering the integration details block. If you do not - * provide a custom template and/or component then the integration will display data as a table of key value - * pairs. - * - * @type Object - * @optional - */ block: { component: { file: './components/block.js' @@ -73,32 +37,15 @@ module.exports = { } }, request: { - // Provide the path to your certFile. Leave an empty string to ignore this option. - // Relative paths are relative to the ServiceNow integration's root directory cert: '', - // Provide the path to your private key. Leave an empty string to ignore this option. - // Relative paths are relative to the ServiceNow integration's root directory key: '', - // Provide the key passphrase if required. Leave an empty string to ignore this option. - // Relative paths are relative to the ServiceNow integration's root directory passphrase: '', - // Provide the Certificate Authority. Leave an empty string to ignore this option. - // Relative paths are relative to the ServiceNow integration's root directory ca: '', - // An HTTP proxy to be used. Supports proxy Auth with Basic Auth, identical to support for - // the url parameter (by embedding the auth info in the uri) - proxy: "" + proxy: '' }, logging: { - level: 'info' //trace, debug, info, warn, error, fatal + level: 'info' }, - /** - * Options that are displayed to the user/admin in the Polarity integration user-interface. Should be structured - * as an array of option objects. - * - * @type Array - * @optional - */ options: [ { key: 'url', @@ -132,29 +79,47 @@ module.exports = { key: 'shouldSearchString', name: 'Search By Annotated Entities', description: - "This will toggle whether or not to search the ServiceNow for annotated entities found in your channels.", + 'This will toggle whether to search ServiceNow for annotated entities found in your channels. The "string" Data Type must also be enabled for this option to have an effect.', default: false, type: 'boolean', userCanEdit: false, adminOnly: true }, + { + key: 'enableIncidentSearch', + name: 'Enable Incident Search', + description: + "If checked, the integration will search ServiceNow's Incident Table (incident) for IP Addresses, Domains, CVEs, annotated entities, and any added custom types", + default: true, + type: 'boolean', + userCanEdit: false, + adminOnly: true + }, { key: 'incidentQueryFields', name: 'Incident Query Fields', description: - 'A comma separated list of Fields to query against Incidents. \n' + - 'NOTE: If a field is not in this list, it will not be searched on Incident Queries.\n' + - '(This applies to IP address, domain, and annotated entity searches)', - default: 'u_ip_addr_2, u_destination_ip, short_description, work_notes', + 'A comma separated list of fields to search when querying for Incidents. Incident searches are done for IPs, domains, CVEs, annotated entities and any added custom types. NOTE: If a field is not in this list, it will not be searched for Incident Queries.', + default: 'short_description, description, work_notes', type: 'text', userCanEdit: false, adminOnly: true }, + { + key: 'incidentDaysAgoToSearch', + name: 'Incident Search Window in Days', + description: + 'Number of days back to search when searching incidents. Filters based on the date that the Incident was opened. Defaults to 360.', + default: 360, + type: 'number', + userCanEdit: false, + adminOnly: true + }, { key: 'enableAssetSearch', name: 'Enable Asset Search', description: - "If checked, the integration will search ServiceNow's Asset Table (alm_asset) for IP Addresses, Domains, CVEs and annotated entities", + "If checked, the integration will search ServiceNow's Asset Table (alm_asset) for IP Addresses, Domains, CVEs, annotated entities, and any added custom types.", default: true, type: 'boolean', userCanEdit: false, @@ -164,10 +129,8 @@ module.exports = { key: 'assetTableFields', name: 'Asset Query Fields', description: - "A comma separated list of fields to search domains and IPs by in ServiceNow's Asset Table. \n" + - "NOTE: If a field is not in this list, the field will not be searched in ServiceNow's Asset Table.\n" + - '(This applies to IP Addresses, Domains, and String searches)', - default: 'dns_domain, sys_domain_path, ip_address, short_description', + "A comma separated list of fields to search when querying for Assets. Asset searches are done for IPs, domains, CVEs, annotated entities and any added custom types. NOTE: If a field is not in this list, the field will not be searched in ServiceNow's Asset Table.", + default: 'name, display_name, asset_tag, ci.name, ci.asset_tag', type: 'text', userCanEdit: false, adminOnly: true diff --git a/config/config.json b/config/config.json index 8d51fda..d85b6ec 100644 --- a/config/config.json +++ b/config/config.json @@ -4,7 +4,14 @@ "acronym": "SN", "defaultColor": "light-purple", "description": "ServiceNow automates and streamlines work and helps create great employee and customer experiences.", - "entityTypes": ["IPv4", "email", "domain", "string", "cve"],"customTypes": [ + "entityTypes": [ + "IPv4", + "email", + "domain", + "string", + "cve" + ], + "customTypes": [ { "key": "incident", "regex": "INC[0-9]{7,}" @@ -26,10 +33,16 @@ "regex": "RITM[0-9]{7,}" } ], - "styles": ["./styles/styles.less"], + "styles": [ + "./styles/styles.less" + ], "block": { - "component": { "file": "./components/block.js" }, - "template": { "file": "./templates/block.hbs" } + "component": { + "file": "./components/block.js" + }, + "template": { + "file": "./templates/block.hbs" + } }, "request": { "cert": "", @@ -38,7 +51,9 @@ "ca": "", "proxy": "" }, - "logging": { "level": "info" }, + "logging": { + "level": "info" + }, "options": [ { "key": "url", @@ -70,25 +85,43 @@ { "key": "shouldSearchString", "name": "Search By Annotated Entities", - "description": "This will toggle whether or not to search the ServiceNow for annotated entities found in your channels.", + "description": "This will toggle whether to search ServiceNow for annotated entities found in your channels. The \"string\" Data Type must also be enabled for this option to have an effect.", "default": false, "type": "boolean", "userCanEdit": false, "adminOnly": true }, + { + "key": "enableIncidentSearch", + "name": "Enable Incident Search", + "description": "If checked, the integration will search ServiceNow's Incident Table (incident) for IP Addresses, Domains, CVEs, annotated entities, and any added custom types", + "default": true, + "type": "boolean", + "userCanEdit": false, + "adminOnly": true + }, { "key": "incidentQueryFields", "name": "Incident Query Fields", - "description": "A comma separated list of Fields to query against Incidents. \nNOTE: If a field is not in this list, it will not be searched on Incident Queries.\n(This applies to IP address, domain, and annotated entity searches)", - "default": "u_ip_addr_2, u_destination_ip, short_description, work_notes", + "description": "A comma separated list of fields to search when querying for Incidents. Incident searches are done for IPs, domains, CVEs, annotated entities and any added custom types. NOTE: If a field is not in this list, it will not be searched for Incident Queries.", + "default": "short_description, description, work_notes", "type": "text", "userCanEdit": false, "adminOnly": true }, + { + "key": "incidentDaysAgoToSearch", + "name": "Incident Search Window in Days ", + "description": "Number of days back to search when searching incidents. Filters based on the date that the Incident was opened. Defaults to 360.", + "default": 360, + "type": "number", + "userCanEdit": false, + "adminOnly": true + }, { "key": "enableAssetSearch", "name": "Enable Asset Search", - "description": "If checked, the integration will search ServiceNow's Asset Table (alm_asset) for IP Addresses, Domains, CVEs and annotated entities", + "description": "If checked, the integration will search ServiceNow's Asset Table (alm_asset) for IP Addresses, Domains, CVEs, annotated entities, and any added custom types.", "default": true, "type": "boolean", "userCanEdit": false, @@ -97,8 +130,8 @@ { "key": "assetTableFields", "name": "Asset Query Fields", - "description": "A comma separated list of fields to search domains and IPs by in ServiceNow's Asset Table. \nNOTE: If a field is not in this list, the field will not be searched in ServiceNow's Asset Table.\n(This applies to IP Addresses, Domains, and String searches)", - "default": "dns_domain, sys_domain_path, ip_address, short_description", + "description": "A comma separated list of fields to search when querying for Assets. Asset searches are done for IPs, domains, CVEs, annotated entities and any added custom types. NOTE: If a field is not in this list, the field will not be searched in ServiceNow's Asset (alm_asset) Table.", + "default": "name, display_name, asset_tag, ci.name, ci.asset_tag", "type": "text", "userCanEdit": false, "adminOnly": true diff --git a/package-lock.json b/package-lock.json index e460030..156c5bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "service-now", - "version": "3.4.2", + "version": "3.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -15,9 +15,9 @@ } }, "@postman/tough-cookie": { - "version": "4.1.2-postman.1", - "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.2-postman.1.tgz", - "integrity": "sha512-keOKL3RQohnH5K4GNGSV7JE8+SU/ktWJ3h9ulqttOGUWCZWcbUOtMNXkC3PQ/R1hIa+2qEfh0/5NCcAhWqeMTQ==", + "version": "4.1.3-postman.1", + "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz", + "integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==", "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -26,9 +26,9 @@ } }, "@postman/tunnel-agent": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.3.tgz", - "integrity": "sha512-k57fzmAZ2PJGxfOA4SGR05ejorHbVAa/84Hxh/2nAztjNXc4ZjOm9NUIk6/Z6LCrBvJZqjRZbN8e/nROVUPVdg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.4.tgz", + "integrity": "sha512-CJJlq8V7rNKhAw4sBfjixKpJW00SHqebqNUQKxMoepgeWZIbdPcD+rguRcivGhS4N12PymDcKgUgSD4rVC+RjQ==", "requires": { "safe-buffer": "^5.0.1" } @@ -68,9 +68,9 @@ "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" }, "aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.1.tgz", + "integrity": "sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA==" }, "base64-js": { "version": "1.5.1", @@ -265,13 +265,13 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "postman-request": { - "version": "2.88.1-postman.32", - "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.32.tgz", - "integrity": "sha512-Zf5D0b2G/UmnmjRwQKhYy4TBkuahwD0AMNyWwFK3atxU1u5GS38gdd7aw3vyR6E7Ii+gD//hREpflj2dmpbE7w==", + "version": "2.88.1-postman.38", + "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.38.tgz", + "integrity": "sha512-gQpK/jjTreGGv7VKeCzwlPa8dulOBANpXfLFuYUEu7sTWQTC6Kv60kfW0z047p7ujMmVwsZkrre/QP/u3DkdnQ==", "requires": { "@postman/form-data": "~3.1.1", - "@postman/tough-cookie": "~4.1.2-postman.1", - "@postman/tunnel-agent": "^0.6.3", + "@postman/tough-cookie": "~4.1.3-postman.1", + "@postman/tunnel-agent": "^0.6.4", "aws-sign2": "~0.7.0", "aws4": "^1.12.0", "brotli": "^1.3.3", @@ -299,9 +299,9 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, "qs": { "version": "6.5.3", @@ -329,9 +329,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -395,4 +395,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 8b6705c..b740613 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "service-now", - "version": "3.4.2", + "version": "3.5.0", "main": "./integration.js", "private": true, "license": "MIT", "author": "Polarity", "dependencies": { "lodash": "^4.17.21", - "postman-request": "^2.88.1-postman.32" + "postman-request": "^2.88.1-postman.38" } } \ No newline at end of file diff --git a/src/dataTransformations.js b/src/dataTransformations.js index 0f7a84f..256adeb 100644 --- a/src/dataTransformations.js +++ b/src/dataTransformations.js @@ -61,7 +61,11 @@ const splitOutIgnoredIps = (_entitiesPartition) => { }; }; -const objectPromiseAll = async (obj = { fn1: async () => {} }) => { +const objectPromiseAll = async ( + obj = { + fn1: async () => {} + } +) => { const labels = keys(obj); const functions = values(obj); const executedFunctions = await Promise.all(map((func) => func(), functions)); @@ -126,6 +130,33 @@ const mapObjectAsync = async (func, obj) => { const parseErrorToReadableJSON = (error) => JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error))); +/** + * Merges two arrays of objects and removes duplicates based on the equality of the + * given `mergeKey` + */ +const mergeAndRemoveDuplicates = (arr1, arr2, mergeKey) => { + // Merge the two arrays + const mergedArray = [...arr1, ...arr2]; + + // Sort the merged array based on the 'key' property + mergedArray.sort((a, b) => { + if (a[mergeKey] < b[mergeKey]) return -1; + if (a[mergeKey] > b[mergeKey]) return 1; + return 0; + }); + + // Filter out duplicates + const uniqueArray = []; + for (let i = 0; i < mergedArray.length; i++) { + // If it's the first element or different from the previous one, add it to the uniqueArray + if (i === 0 || mergedArray[i][mergeKey] !== mergedArray[i - 1][mergeKey]) { + uniqueArray.push(mergedArray[i]); + } + } + + return uniqueArray; +}; + module.exports = { getKeys, groupEntities, @@ -135,5 +166,6 @@ module.exports = { mapObject, mapObjectAsync, transpose2DArray, - parseErrorToReadableJSON + parseErrorToReadableJSON, + mergeAndRemoveDuplicates }; diff --git a/src/displayStructures/assets.js b/src/displayStructures/assets.js index d6ddfeb..9b5b710 100644 --- a/src/displayStructures/assets.js +++ b/src/displayStructures/assets.js @@ -7,6 +7,7 @@ const ASSET_DISPLAY_STRUCTURE = [ isDisplayLink: true }, { label: 'Asset Name', path: 'name' }, + { label: 'Name', path: 'ci.link', pathIsLinkToMoreData: true, pathToOnePropertyFromMoreDataToDisplay: 'name'}, { label: 'Display Name', path: 'display_name' }, { label: 'Asset Subcategory', path: 'subcategory' }, { label: 'Serial Number', path: 'serial_number' }, diff --git a/src/functionalityByEntityType/assetsAndIncidentCustomFunctionality.js b/src/functionalityByEntityType/assetsAndIncidentCustomFunctionality.js index f895286..8cb7d4b 100644 --- a/src/functionalityByEntityType/assetsAndIncidentCustomFunctionality.js +++ b/src/functionalityByEntityType/assetsAndIncidentCustomFunctionality.js @@ -15,10 +15,15 @@ const { const assetsAndIncidentCustomFunctionality = { queryFunction: async (entity, options, requestWithDefaults, Logger) => ({ - ...(options.enableAssetSearch ? await queryAssets(entity, options, requestWithDefaults, Logger) : {}), - ...(await queryTableData(entity, options, requestWithDefaults, Logger)) + ...(options.enableAssetSearch + ? await queryAssets(entity, options, requestWithDefaults, Logger) + : {}), + ...(options.enableIncidentSearch + ? await queryTableData(entity, options, requestWithDefaults, Logger) + : {}) }), - tableQueryQueryString: ({ value }, { incidentQueryFields }) => + tableQueryTableName: 'incident', + tableQueryQueryString: ({ value }, { incidentQueryFields, incidentDaysAgoToSearch }) => incidentQueryFields && typeof incidentQueryFields === 'string' && incidentQueryFields.length !== 0 && @@ -26,7 +31,10 @@ const assetsAndIncidentCustomFunctionality = { split(','), compact, map((field) => `${trim(field)}CONTAINS${value}`), - join('^NQ') + join('^OR'), + (query) => { + return `${query}^opened_at>javascript:gs.daysAgoStart(${incidentDaysAgoToSearch})`; + } )(incidentQueryFields), createSummaryTags: (results, entity, Logger) => getTableQueryDataSummaryTags(results, entity, Logger).concat( diff --git a/src/functionalityByEntityType/customFunctionalityByType.js b/src/functionalityByEntityType/customFunctionalityByType.js index 94580f1..020f384 100644 --- a/src/functionalityByEntityType/customFunctionalityByType.js +++ b/src/functionalityByEntityType/customFunctionalityByType.js @@ -4,7 +4,10 @@ const { mapObject } = require('../dataTransformations'); const queryKnowledgeBase = require('../querying/queryKnowledgeBase'); -const { getTotalKbDocsSummaryTag } = require('./createSummaryTagsFunctions'); +const { + getTotalKbDocsSummaryTag, + getTableQueryDataSummaryTags +} = require('./createSummaryTagsFunctions'); const assetsAndIncidentCustomFunctionality = require('./assetsAndIncidentCustomFunctionality'); @@ -12,19 +15,31 @@ const { knowledgeBaseDisplayStructure, usersDisplayStructure } = require('../displayStructures/index'); +const queryTableData = require('../querying/queryTableData'); +const { tableQueryDisplayStructure } = require('../displayStructures'); +const numberTableQueryString = ({ value }) => `number=${value}`; +const BUILT_IN_SERVICENOW_TYPE_DEFAULTS = { + queryFunction: queryTableData, + tableQueryTableName: 'incident', + tableQueryQueryString: numberTableQueryString, + createSummaryTags: getTableQueryDataSummaryTags, + displayTabNames: { tableQueryData: 'Incidents' }, + displayStructure: { tableQueryData: tableQueryDisplayStructure }, + tableQuerySummaryTagPaths: ['category', 'phase'] +}; /** CUSTOM_FUNCTIONALITY_FOR_STANDARD_ENTITY_TYPES - * This is where all the magic stems from. This object is where can you specify custom + * This is where all the magic stems from. This object is where can you specify custom * functionality for any standard (non-custom) entity type without really having to - * understand all the gotcha's of the rest of the code and allows you to quickly and - * easily add new type to the integration. In this object the keys are the entity type - * names, and the values are where you specify you custom functionality for that entity + * understand all the gotcha's of the rest of the code and allows you to quickly and + * easily add new type to the integration. In this object the keys are the entity type + * names, and the values are where you specify you custom functionality for that entity * type. * { - * [result of entity.type check]: { + * [result of entity.type check]: { * This object that describes querying and formatting functionality for this entity type - * + * * queryFunction: [default = queryTableData] * async (entity, options, requestWithDefaults, Logger) => { * ...Query code @@ -34,37 +49,37 @@ const { * } * }, check out use in getLookupResults.js for more info * NOTE: It is recommended you create this function in the ./querying folder and import it here. - * + * * tableQueryTableName: [default = "incidents"] * "String used to specify the table name for the Table Query" * NOTE: Omit this key if you use a queryFunction that does not execute queryTableData. - * Can be found one an individual record for the table on the dashboard under the + * Can be found one an individual record for the table on the dashboard under the * "uri" query parameter in the dashboard url excluding the / or %2F, and the ".do" * tableQueryQueryString: [default = numberTableQueryString] * (entity, options) => "String used to specify the query string for the Table Query" * NOTE: Omit this key if you use a queryFunction that does not execute queryTableData. * tableQuerySummaryTagPaths: [default = ["category"]] * ["List of string paths of properties you would like to display from Table Query results"] - * NOTE: The defaults will always be added to your result. Also, omit this key if - * you use a queryFunction that does not execute queryTableData Or if only the + * NOTE: The defaults will always be added to your result. Also, omit this key if + * you use a queryFunction that does not execute queryTableData Or if only the * default paths are needed. - * + * * createSummaryTags: [default = getTableQueryDataSummaryTags] * (entity, results) => { * return ["Summary Tags generated from results for this entity type"]; * } * results is the result object generated in the query function * NOTE: It is recommended you add your custom functionality to createSummaryTagsFunctions.js - * + * * displayTabNames: [default = { tableQueryData: "Incidents" }] * { [key returned from queryFunction]: "Tab name for this query result"} * displayStructure: [default = { tableQueryData: tableQueryDisplayStructure }] * { [key returned from queryFunction]: [{displayStructure}]} * NOTE: A Display Structure shows how we format the data for the front end to display. * Look to ./displayStructures/tableQuery for reference. -* } - * NOTE: If you would like to just use the defaults for an entity type, it can just be - * omitted from this object. If you would like to understand the defaults better you + * } + * NOTE: If you would like to just use the defaults for an entity type, it can just be + * omitted from this object. If you would like to understand the defaults better you * check out the DEFAULT_FUNCTIONALITY_OBJECT in defaultFunctionalityByType.js */ @@ -84,29 +99,27 @@ const CUSTOM_FUNCTIONALITY_FOR_STANDARD_ENTITY_TYPES = { /** CUSTOM_FUNCTIONALITY_FOR_CUSTOM_ENTITY_TYPES * Custom Entity Types work the same as standard entity types with two differences: - * 1.) The Keys on the top level are not the result of the entity.type check, but + * 1.) The Keys on the top level are not the result of the entity.type check, but * instead are the "key" properties specified in the config.js file. - * 2.) Properties in "defaults" get passed along to to all specific custom types, but + * 2.) Properties in "defaults" get passed along to to all specific custom types, but * can be overridden if specified on the individual custom type level. * - * NOTE: If you would like to just use the defaults for an entity type, it can just be - * omitted from this object. If you would like to understand the defaults better you + * NOTE: If you would like to just use the defaults for an entity type, it can just be + * omitted from this object. If you would like to understand the defaults better you * check out the DEFAULT_FUNCTIONALITY_OBJECT in defaultFunctionalityByType.js -*/ + */ const CUSTOM_FUNCTIONALITY_FOR_CUSTOM_ENTITY_TYPES = { - // All Custom Types - defaults: { - tableQuerySummaryTagPaths: ['category', 'phase'] - }, + // All Custom Type Defaults + defaults: BUILT_IN_SERVICENOW_TYPE_DEFAULTS, - // Specific Custom Types + // Specific Custom Types which override default above which is to search both incident and asset + // tables. knowledgeBase: { queryFunction: queryKnowledgeBase, createSummaryTags: getTotalKbDocsSummaryTag, displayTabNames: { knowledgeBaseData: 'Knowledge Base' }, displayStructure: { knowledgeBaseData: knowledgeBaseDisplayStructure } }, - change: { tableQueryTableName: 'change_request', displayTabNames: { tableQueryData: 'Changes' } @@ -118,7 +131,8 @@ const CUSTOM_FUNCTIONALITY_FOR_CUSTOM_ENTITY_TYPES = { requestedItem: { tableQueryTableName: 'sc_req_item', displayTabNames: { tableQueryData: 'Requested Items' } - } + }, + incident: {} }; const specificCustomTypesWithDefaults = flow( @@ -137,5 +151,4 @@ const customFunctionalityByType = { ...specificCustomTypesWithDefaults }; - -module.exports = customFunctionalityByType; \ No newline at end of file +module.exports = customFunctionalityByType; diff --git a/src/functionalityByEntityType/defaultFunctionalityByType.js b/src/functionalityByEntityType/defaultFunctionalityByType.js index 38f8671..44b0fce 100644 --- a/src/functionalityByEntityType/defaultFunctionalityByType.js +++ b/src/functionalityByEntityType/defaultFunctionalityByType.js @@ -1,15 +1,19 @@ const { map, flow, get, reduce } = require('lodash/fp'); -const { entityTypes, customTypes } = require('../../config/config'); - +const { mergeAndRemoveDuplicates } = require('../dataTransformations'); +const { entityTypes, customTypes: customTypesJs } = require('../../config/config'); +const { customTypes: customTypesJson } = require('../../config/config.json'); +const customTypes = mergeAndRemoveDuplicates(customTypesJs, customTypesJson, 'key'); +const assetsAndIncidentCustomFunctionality = require('./assetsAndIncidentCustomFunctionality'); const queryTableData = require('../querying/queryTableData'); const numberTableQueryString = ({ value }) => `number=${value}`; - const { getTableQueryDataSummaryTags } = require('./createSummaryTagsFunctions'); const { tableQueryDisplayStructure } = require('../displayStructures/index'); +// Not currently used but contains default values if you only want to search the Incident +// table and not search the assets table. const DEFAULT_FUNCTIONALITY_OBJECT = { queryFunction: queryTableData, tableQueryTableName: 'incident', @@ -17,8 +21,7 @@ const DEFAULT_FUNCTIONALITY_OBJECT = { createSummaryTags: getTableQueryDataSummaryTags, displayTabNames: { tableQueryData: 'Incidents' }, displayStructure: { tableQueryData: tableQueryDisplayStructure }, - // Empty Defaults - tableQuerySummaryTagPaths: false + tableQuerySummaryTagPaths: ['category', 'phase'] }; const defaultFunctionalityForStandardEntityTypes = reduce( @@ -30,7 +33,7 @@ const defaultFunctionalityForStandardEntityTypes = reduce( const defaultFunctionalityForCustomEntityTypes = flow( map(get('key')), reduce( - (agg, entityType) => ({ ...agg, [entityType]: DEFAULT_FUNCTIONALITY_OBJECT }), + (agg, entityType) => ({ ...agg, [entityType]: assetsAndIncidentCustomFunctionality }), {} ) )(customTypes); diff --git a/src/querying/queryAssets.js b/src/querying/queryAssets.js index fb02a1a..092e48e 100644 --- a/src/querying/queryAssets.js +++ b/src/querying/queryAssets.js @@ -7,7 +7,7 @@ const queryAssets = async (entity, options, requestWithDefaults, Logger) => { get('assetTableFields'), split(','), map((field) => `${trim(field)}CONTAINS${entity.value}`), - join('^NQ') + join('^OR') )(options); const requestOptions = {